程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,内存分配和内存回收都具备确定性,所以不需要过多考虑这几个区域的回收问题。这里我们主要探究Java堆的回收机制。
如何判断对象可被回收
-
引用计数机制
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。
该算法实现简单,判定效率高,但因为无法解决对象之间相互循环引用的问题,现今主流的Java虚拟机并没有选用它来管理内存。 -
可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。
Java对象中,可作为GC Roots的对象:
1. 虚拟机栈中引用的对象。
2. 方法区中类静态属性引用的对象。
3. 方法区中常量引用的对象。
4. 本地方法栈中JNI引用的对象。
-
一定会被回收吗?
在可达性分析算法中,即使被判断为不可达的对象,也不一定会被回收。如果一个对象实现了finalize()方法,并且该方法还没有被虚拟机调用过,则在回收之前会调用此方法,若在此方法中,将该对象与任何一个引用链上的对象关联,则在finalize()调用后可以成功的变成可达的对象,从而避免被回收。 -
方法区的回收
虽然说方法区回收的比率非常低,但也不是不能回收。
在常量池中,如果某个常量不被任何地方引用,则可以被清理出常量池。
判断一个类是否是“无用的类”的条件苛刻许多,需要同时满足以下三个条件:
1. 该类所有的实例都已经被回收。
2. 加载该类的ClassLoader已经被回收。
3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一般情况下我们不需要太关心方法区的回收问题,但如果项目中大量使用到反射、动态代理等频繁自定义ClassLoader的场景则需要虚拟机具备类卸载的功能,以保证不会内存溢出。
垃圾收集算法
-
标记 - 清除算法(Mark-Sweep)
最基础的收集算法。首先标出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
该算法有两个不足之处:一个是效率问题,标记和清除两个过程的效率都不高;另一个是标记清除之后会产生大量不连续的内存碎片,当遇到需要分配较大内存的对象时,会因为无法找到足够的连续内存而提前触发一次GC。 -
复制算法(Copying)
该算法将可用的内存安容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把以使用过的内存空间一次性清理掉。
该算法的代价是将可用内存缩小为了原来的一半。
但是实际情况中我们并不会真正按照1:1来划分内存空间,有研究表明新生代中98%的对象都是“朝生夕死”的,所以实际情况中将内存空间分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时将Eden和Survivor中还存活的对象复制到另一块上,然后清除掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的比例是8:1。这样只需要付出10%的内存,就能在绝大部分情况下顺利完成回收工作,当Survivor空间不够用时,则需要依赖其他内存进行分配担保。 -
标记 - 整理算法(Mark - Compact)
复制算法不适用于回收率低的情况,所以老年代采用了“标记 - 整理”算法,标记过程依然与“标记 - 清除”算法一样,而后续步骤则是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 -
分代收集算法(Generational Collection)
该算法根据对象存活周期的不同将内存划分为几块,一般是把Java堆氛围新生代和老年代,新生代的回收率非常高,则选用复制算法;老年代的对象存活率高、没有额外的空间对它进行分配担保,就必须使用“标记 - 清理”或者“标记 - 整理”算法来进行回收。
网友评论