一、虚拟机内存分区
java虚拟机运行在受不同操作系统操纵的物理机上,不同的操作系统使用不同的底层方法来执行不同的操作,这些方法称之为本地方法:Native Method,本地方法一般执行的都是比较底层的操作,比如说IO、线程管理等,java方法则会执行的一般是相对高级的操作,比如说数逻运算,或者是调用底层的本地方法来完成底层任务。java虚拟机的运行时数据区域将内存分成了不同的部分协调完成java虚拟机的内存数据交互。
按照数据存储过程的数据结构可以大致分为:
栈区:
- 虚拟机栈:java虚拟机运行的java方法(java字节码方法)构成的栈空间,这个空间在运行时存储这些方法的局部变量表、操作栈、动态链接和方法出口;
- 本地方法栈:本地方法在运行时存储数据产生的栈区。
堆区:
- java堆:对象的实例存储在这个共享的堆空间里,由于占有最大的和最有实际意义的空间,这个空间的GC过程时虚拟机运行的重点。
- 方法区:存储虚拟机运行时加载的类信息、常量、静态变量和即时编译的代码,因此可以把这一部分考虑为一个保存相对来说数据较为固定的部分,常量和静态变量在编译时就确定下来进入这部分内存,运行时类信息会直接加载到这部分内存,所以都是相对较早期进入内存的。
- 运行时常量池:不是所有的常量都是在编译时就确定下来进入内存的,仍然会有运行时才进入内存的常量,这部分常量一般是编译时产生的一些固定信息,比如说翻译出的引用等,直接在类加载的时候把它们存入运行时常量池有助于提高性能。 所有的内存区域的数据交互由程序计数器指导虚拟机完成复杂的逻辑步骤。
如何找到一个对象的实例:
全选复制放进笔记
Object obj = new Object();
在这个过程中在虚拟机栈的局部变量表里创建obj引用,在堆内存里创建Object类的一个实例,最后就是把obj引用和这个对象实例关联起来的问题了,另外,我们需要知道的是,不是所有的实例都完整地保存了所有的类的信息,一般共有的或者静态的类的数据将被保存在方法区中,独有的实例数据才会真的被保存在java堆里,因此每个引用必须同时找到关联它的实例数据和类数据。针对这个问题,有两个办法来做:
I. 引用存储的只是实例的句柄,句柄在堆的句柄池中,句柄中保存着到堆中真正实例的地址和到方法区中类数据的地址,这样就可以通过这个句柄可以找到这些地址。
II. 引用存储的就是实例在堆中的地址,而实例中是含有可以定位类数据的地址的,也就是通过找到的实例地址可以再去寻找它对应的类的数据。
两个和内存溢出相关的异常:
- StackOverflowError:线程申请的栈深度大于虚拟机的规定值;
- OutOfMemoryError:线程扩展增加的内存大于虚拟机的要求;
二、内存回收机制
虚拟机栈、本地方法栈和计数器大都是编译期确定的内存分配,在线程执行完毕后即会清理,内存回收相对比较容易。所以我们提到的内存回收大都是指堆内存的回收。我们通过如下几个问题来说明内存回收机制:
1. 什么样的堆内存是可以回收的呢?
什么样的堆内存是可以回收的呢?简而言之就是那些“没用”的内存,那么怎样的内存是“没用”的呢?即那些通过现有的指针(或称“引用”)条件下再也访问不到的内存对象。所以有这样的算法来描述无效的引用: (引用计数算法)每个对象都有一个被引用计数器,被引用一次计数器加1,引用被置空时减1,最终被引用计数器的值为0 的即是“无用”的内存对象,它占用的内存可以被回收。 (这个算法看起来好像没有问题,但是遭遇到循环引用的时候就会出现问题:如果同时将循环引用的双方置空,那么即使被引用计数器不为0也再也访问不到这些对象了,即发生了内存无故占用)。
这个过程体现了互相循环引用可能带来的问题,对象仍被引用但是已经不能被访问了,所以是这种算法的缺陷。
(根搜索算法)将由栈内存或方法区引用的对象作为GCRoots去构建引用链,如果能找到这个对象则说明这个对象能够访问其内存不能被回收,反之通过这些引用链找不到这个对象则说明已经是弃用的对象了,其内存是应该被回收的。(上面的互相循环引用的例子就可以解决了,因为这个问题里面虽然其被引用计数器的值不为0,但是已经没有GCRoots能够找到这些内存了,这个问题里的GC Roots是栈内存里的objA和objB,这两个栈内存里的引用被置空,因此引用链里没办法再找到对内存里的对象了。)
2. 确定了有哪些内存该被回收后GC机制是直接回收内存吗?
确定了有哪些内存该被回收后GC机制是直接回收内存吗?GC会给这些内存中的某些对象一次机会,就是那些重写过finalize方法的类的对象,GC会执行这个对象重写过的finalize方法,如果在这个方法中对象重新将自己链接给了某个引用使得这块内存区域重新可以被访问,那么GC就不会在这次回收它,但是,这个过程只能执行一次,下一次再被GC遇到的话就不会顾及这个finalize方法而是直接回收了,因此要注意重写的finalize方法只能执行一次。 这个是堆内存中对象的回收,在方法区里保存类信息和常量池的内存同样需要回收,这个过程相对来说更缓慢也并没有那么高效,因为一段时间内线程使用的类和常量池都比较稳定,只有当真的确认有类不再使用且不被反射使用的时候才会卸载类,当真的没有常量再被使用的时候才会释放常量池中不用的常量。
参见:http://segmentfault.com/a/1190000003834613