Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来
GC要解决的问题无非是以下三个:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
本文将回答第一点和第三点问题。
哪些内存需要回收?
垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中,哪些还“存活”,哪些已经死去,这里有两种常用的判断算法,引用计数算法和可达性分析算法。
- 引用计数算法:Python使用了该算法,其原理为给对象中添加一个计数器,每当一个地方引用它时,计数器加1,引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。但是一个缺点是无法解决对象之间相互循环引用的问题。如下图,objA中引用objB,objB引用objA。
- 可达性分析算法:被Java、C#等语言所使用的,这个算法的基本思路是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链想连时,则证明此对象是不可用的。 在Java中,可作为GC Roots的对象包括下面几种:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象
死亡前的自救
即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时出于“缓刑”阶段,真正宣告一个对象死亡,至少经历两次标记过程。 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,并判断该对象是否有必要执行finalize()方法; 如果有必要执行finalize()方法,那么对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级Finalizer线程去执行它。 这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。finalize()是对象逃脱死亡命运的最后一次机会,如果在finalize()中对象与任何一个对象建立了联系,那么在接下来的第二次标记时,就会被移出“即将回收”的集合。
垃圾收集算法
-
标记-清除算法:分为“标记”和“清除”两个阶段;有两个缺陷,一是效率不高,二是空间碎片太多
-
复制算法:它将可用内存按容量划分为大小相等的两部分,如图所示分为A、B两个区,当A区域全部用完时,把存活的对象复制到B区,然后一次性清除掉A区,此时,内存分配时就不再考虑内存碎片等复杂情况了。而且新生代中的对象98%是朝生夕死的,并不需要按照1:1来划分内存空间。
Hotspot虚拟机中Eden和Survivor的大小比例是8:1,将Eden区以及幸存区From中存活的对象复制到幸存区To中,将Eden和幸存区From中的对象全部清空,然后将幸存区From和幸存区To调换。
如果Survivor空间不够用时需要依赖其他内存(老年代)进行分配担保,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象直接通过分配担保机制进入老年代。
该算法适用于对象存活率较低的新生代。
-
标记-整理算法:如果对象存活率较高时,需要进行较多的复制操作,效率会变低。所以在老年代不选用这种算法。标记-整理算法的标记过程与标记-清理算法一致,但是清理过程是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。
HotSpot的算法实现
枚举根节点
可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),由于方法区庞大以及在进行可达性分析的时候需要“Stop the world”,因此要求虚拟机在枚举根节点的时候需要快速。 HotSpot虚拟机是准确式GC(虚拟机知道内存中存储的是地址还是数),所以当执行系统停顿下来的时候,虚拟机有办法直接得知哪些地方存放着对象引用。HotSpot使用一组称为OopMap的数据结构来记录对象内什么偏移量上是什么类型的数据计算出来。
安全点
- 安全点的选取:如果在每一条指令都生成对应的OopMap,那将会需要大量的额外空间,因此,HotSpot只在特定的位置记录这些信息,这些位置称为安全点。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
- 多线程同步停止:有两种方式,抢先式中断和主动式中断;抢先式中断在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上,但是这种方法没有被主流虚拟机采用;主动式中断的思想是当GC需要中断线程的时候,仅简单的设置一个标志,各个线程主动去轮训这个标志,发现中断标志位真时就自己中断挂起。轮训标志的地方和安全点是重合的。
安全区
如果线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,这时候需要安全区。所谓安全区就是指在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。 在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,JVM就可以发起GC;当线程离开Safe Region时,它要检查系统是否完成了根节点的枚举(或是整个GC过程)。