JVM专题:引用计数和可达性分析算法(存活性判断)
目前虚拟机基本都是采用可达性分析算法进行GC,没有使用引用计数法。
题外话:C++ 11的智能指针就是基于引用计数的,它提供了weak_ptr来解决循环引用的问题。(类似于Java中的WeakReference,作用类似)
1 引用计数算法
1.1 算法思想
在对象头处维护一个引用计数器counter,每当有一个地方引用它时,计数器值就加1;
当引用失效时,计数器值就减1;
任何时候计数器为0时的对象就是不能再被使用。
1.2 特点
- 优点:实现简单;判定效率高。
- 缺点:很难解决对象之间相互循环引用的问题。(所以虚拟机不是通过引用计数算法判断对象是否存活)
2 可达性分析算法
这是目前主流的虚拟机都是采用GC Roots Tracing 算法,比如Sun的Hotspot虚拟机便是采用该算法。
2.1 算法思想
可达性分析是基于图论的分析方法,它会找一组对象作为GC Root(根结点),并从根结点进行遍历,遍历走过的路径称为引用链,遍历结束后如果发现某个对象是不可达的(即从GC Root到此对象没有路径),那么它就会被标记为不可达对象,等待GC。
如果对象在进行可行性分析后发现没有与GC Roots相连的引用链。它会暂时被标记上并且进行一次筛选,筛选的条件是是否与必要执行finalize()方法。如果被判定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动建立的、低优先级的线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。如果这时还是没有新的关联出现,那基本上就真的被回收了。
可达性分析算法是通过枚举根节点来实现的,最重要的问题是GC停顿。为了确保一致性(即所有对象之间的关系是确定下来的)而导致GC进行时必须进行停顿。在HotSpot的中,使用OopMap的数据结构存储特定位置上的调试信息,存储栈上那个位置原来是什么东西,这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。 这样,GC在扫描时就可以得知这些信息了。这样做的目的是使HotSpot能够快速准确的完成GC Roots枚举,以期望减少GC停顿所带来的影响。HotSpot没有在所有的指令生成OopMap,所以只是在“特定位置”记录这些信息,这些位置就是安全点。程序执行时并非在所有的位置上都能停顿下来GC,只有在到达安全点时才能暂停。安全点选取基本上是以“是否让程序长时间执行的特征”选定。此外,HotSpot虚拟机在安全点的基础上还增加了安全区域的概念,安全区域是安全点的扩展。在一段安全区域中能够实现安全点不能达成的效果。
参考文献:《深入理解Java虚拟机——JVM高级特性与最佳实践》 第二版
2.2 GC Roots的对象
能作为GC Root的对象必定为可以存活的对象,比如全局性的引用(静态变量和常量)以及某些方法的局部变量(栈帧中的本地变量表)。
以下对象通常可以作为GC Root:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类静态属性以及常量引用的对象
- 本地方法栈中JNI引用的局部变量以及全局变量的对象
3 对象存亡判断
3.1 两次标记过程
真正宣布一个对象已死,必须经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,则将会被第一次标记并且进行一次筛选;
- 若这个对象被判定有必要执行
finalize()方法,则该对象放置在一个叫F-Queue队列中,并之后由VM自动建立的、低优先级的Finalizer线程去执行它。 - finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue队列中的对象进行第二次小规模的标记,若finalize()能够救活自己,则第二次标记时,将被移除出“即将回收”的集合;若对象没有完成自救,则回收。
3.2 补充
- 筛选的条件:这个对象有没有必要去执行finalize()方法?当对象没有覆盖finalize()方法或者finalize()方法已经被VM掉用过,则“没有必要执行“
- “执行”操作的触发:是VM会触发这个方法,而不承诺等它运行结束,原因:如果一个对象在finalize()方法中执行缓慢,或发生死循环,将导致F-Queue队列中其他对象永远处于等待,甚至导致整个内存回收系统崩溃。
- finalize()中对象自救:只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量。finalize()最多只会被系统自动调用一次,所以自救机会只有一次。finalize()方法的运行代价搞,不确定大,无法保证各个对象的调用顺序。

浙公网安备 33010602011771号