《Understanding the JVM》读书笔记之二——垃圾回收算法
垃圾收集器工作的第一步就是判断对象是否还活着,通过垃圾回收算法判断。
一、引用计数算法
• 在对象A中添加一个引用计数器,当有一个地方引用A时,计数器+1;当引用失效时,计数器-1,任何时刻计数器数值为0时,这个对象就不会再被使用了;
• 引用计数法的实现简单,判断效率高。但再主流的java虚拟机中没有使用此算法,原因是,它无法解决相互循环引用问题。
二、可达性分析算法:(不可达意味着对象死亡的可能性高)
• 通过一系列GC roots 对象作为起始点,从GC Roots开始向下搜索,所走过的路径称为 引用链,当一个对象到GC Root没有任何引用链时,则证明此对象是不可用的。
• Java中可以被看作GC Root的对象包括:
a. 虚拟机栈中引用的对象(栈帧中的局部变量表)
b. 方法区中类静态属性引用的对象
c. 方法区中常量引用的对象
d. 本地方法栈中JNI(Native)方法引用的对象。
三、生存还是死亡?(即使是不可达的对象,也不是非死不可)
1. 查找引用链
• 当对象在可达性分析后发现没有与GC Root相连的引用链,此对象会被标记(第一次),并进行一次筛选。
2. 第一次筛选
• 筛选的条件:此对象是否需要执行finalize()方法,当此对象没有覆盖finalize()方法或finalize()方法已经被调用过=>没有必要执行,放入被会收集和;否则==>有必要执行;
3. 放入队列
• 如果判定有必要执行finalize,此对象会先被放到F-Queue队列中,
4. Finalizer线程
• 稍后,虚拟机会自动建立Finalizer线程,对F-Queue中的对象触发finalize()方法,但finalize()方法并不一定会执行完毕。(原因:如果finalize()方法执行缓慢、或死循环将导致整个队列等待)
5. 第二次标记
• 再稍后,GC将对F-Queue中的对象进行第二次标记(逃脱死亡的最后一次机会),标记原则:如果此对象再finalize()方法中与GC Root引用链上的任何对象建立关联==>移出”被回收对象集合“;否则==>不移出。
6. GC回收
四、方法区是否有必要回收
• 相比之下,新生代中GC回收效率较高,常规应用一次GC可以回收70~90%的空间;
• 永久代(方法区)回收性价比较低,主要回收:废弃常量、无用的类两种。而在jdk1.7以后,常量池已经移至堆内存,这部分也不应该计算在内。
• 无用的类需要满足以下三个条件:
a. 该类所有实例都被回收
b. 加载该类的ClassLoader已经被回收
c. 该类对应的java.lang.Class对象没有在任何地方引用(没有反射创建类)
五、常见算法实现
标记-清除算法:
1. 对所有要回收的对象进行标记(见生存还是死亡)
2. 回收被标记的对象
3. 算法问题:
• 效率:标记和清除效率都不高
• 空间:清除后会产生大量内存碎片,在之后分配大对象时无法找到足够的连续内存而提前触发第二次GC
复制算法:
1. 将内存容量划分为大小相等的两块,每次只是用其中的一块,
2. 当一块内存使用满,将还存活着的对象复制到另一块内存,
3. 一次性清除已满的内存块。
4. 算法问题:
• 对整个区域进行回收,不需要考虑内存碎片问题,而且指针按顺序移动,效率高。
• 但代价是可用内存减少到一半
• 在对象存活率较高的情况下需要进行较多的复制操作,效率会变低
5. 现代商业虚拟机都采用这种算法回收新生代,但比例不是1:1,因为新生代对象有98%左右朝生夕死:
• HotSpot虚拟机将新生代内存划分为一块较大的Eden区和两个较小的Survivor区,E,S比例为8:2。
• 每次使用其中的Eden区和一个Survivor区,当GC回收时,将所有存活的对象复制到另一个Suivivor区中,这样只有10%的内存被“浪费”。
• 而当另外的Survivor区内存不够分配时,需要依赖其他内存进行担保(老年代)
标记-整理算法:
1. 标记过程同 标记-清除
2. 标记后让所有对象都向一端移动,然后直接清理掉边界以外的内存
3. 主要用于老年代的回收
分代收集算法:
• 当前商业虚拟机都使用分代收集:将所有对象按照生命周期,将内存划分为几块,一般划分为:新生代和老年代。
• 对于新生代,每次GC都会回收大量对象,使用复制算法
• 对于老年代,对象存活率较高,不需使用标记-清除或标记-整理算法来回收
六、HotSpot虚拟机算法实现:
1. 枚举根节点
• 在可达性分析中,GC Root节点主要在全局引用和执行上下文中,在大型应用中(方法区几百兆)必然耗费很多时间,另外,为了确保一致性,在可达性分析的过程中,整个系统必须停顿(stop the world),即使在CMS收集器(号称不会停顿)也必然产生停顿。
• 大部分Java虚拟机都是用准确式GC,虚拟机能够知道那些地方存储着对象引用(HotSpot使用OopMap数据结构来实现),防止全局扫描。
• 在HotSpot中,类加载时就把对象什么偏移量上对应的数据类型计算出来;在编译过程中,也会在特定位置记录下栈和寄存器中哪些位置是引用,这样GC扫描时就可以直接得知。
2. 安全点
• 特定位置称为安全点==>程序执行时只有在到达安全点时才能开始GC。这就需要考虑两个问题:
• Safepoint即不能太少也不能太多,太少会让GC等待时间过长,太多会增大运行时的负荷。
• 如何在GC发生时,让所有线程都停在最近的安全点上。主要有抢先式中断和主动式中断两种方式。
• 抢先式中断==>在GC时首先把所有线程强制中断,如果发现有没到达安全点的线程,就恢复该线程,让它“跑”到安全点上。现在几乎没有虚拟机使用
• 主动式中断==>当需要GC时,不主动中断线程,只设置一个标志,各个线程主动轮询这个标志,发现中断标志为真时,线程自己中断挂起。而轮询标志的地方是安全点重合+创建对象需要分配内存的地方
3. 安全区域
• 为了应对线程不执行的时候(没有抢占到CPU时间或线程sleep),无法响应线程中断的请求,线程也就无法暂停的问题,引入了安全区。
• 安全区是在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。
• 在GC时不用管自己标识为SafeRegion的线程了,在线程要离开SafeRegion时,需要检查系统是否已经完成了根节点枚举,完成则继续执行,否则等待收到安全离开SafeRegion的信号为止。