✅JVM如何判断对象是否存活?

典型回答

当JVM判断对象不再存活的时候,便会在下一次GC时候将该对象回收掉,为堆腾出空间,那么JVM如何判断对象是否存活呢?

JVM有两种算法来判断对象是否存活,分别是引用计数法可达性分析算法


  • 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

循环引用会导致对象无法被回收,最终会导致内存泄漏及内存溢出

  • 可达性分析算法: 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

但是,并不是说当进行完可达性分析算法后,即可证明某对象可以被GC。对象是否存活,需要两次标记:

  1. 第一次标记通过可达性分析算法。如果没有GC Roots相连接的引用链,那么将第一次标记
  2. 如果对象的finalize()方法被覆盖并且没有执行过,则放在F-Queue队列中等待执行(不一定会执行),如果一段时间后该队列的finalize()方法被执行且和GC Roots关联,则移出“即将回收”集合。如果仍然没有关联,则进行第二次标记,才会对该对象进行回收

不过现在都不提倡覆盖finalize方法,它的本意是像Cpp一样在对象销毁前执行,但是它影响了JAVA的安全和GC的性能,所以第二种判断会越来越少

知识扩展

哪些内容可以作为GC roots?

GC roots是作为可达性分析算法的起点的。要实现语义正确的可达性分析,就必须要能完整枚举出所有的GC Roots,否则就可能会漏扫描应该存活的对象,导致GC错误回收了这些被漏扫的活对象。那么,所谓“GC Roots”,就是一组必须活跃的引用

那么,有哪些引用是一定活跃的呢?看下下面这些是不是都符合这个条件:

  • Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。
  • Thread - 活着的线程
  • Stack Local - Java方法的local变量或参数
  • JNI Local - JNI方法的local变量或参数
  • JNI Global - 全局JNI引用
  • Monitor Used - 被同步锁(synchronized)持有的对象
  • Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。

以上,比如系统类加载器加载的对象、活着的线程、方法中的本地变量、被synchronized锁定的对象这些,都是符合活跃的引用这个条件的!

除了这些, 还有一种,基本上很少有人提到的,大家去看网上的所有关于介绍GC Root的八股文,基本上没人提的,那就是为了解决跨代引用的问题,会把Remembered Set也作为GC Root。

✅什么是跨代引用,有什么问题?

可达性分析算法的不足

虽然可达性分析算法相比于引用计数法要好很多,但是他也不是毫无缺点的。这种算法主要存在以下几个不足:

STW时间长

可达性分析算法需要对程序进行全局分析,因此时间复杂度较高,可能需要很长的时间才能完成分析,并且整个过程都是STW的,所以对应用的整体性能有很大影响。这也使得可达性分析算法难以适用于大型程序的分析。所以一些常见的回收器都会使用一些优化技术来减少可达性分析的时间和开销,如增量标记、增量拷贝等。

解决这个问题,主要是依赖三色标记法

✅什么是三色标记算法?

存消耗

可达性分析算法需要存储程序中所有的对象和它们之间的引用关系,这些信息需要占用大量的内存空间。对于大型程序,如果要进行完整的可达性分析,需要存储的对象数量和引用关系数量都非常大,可能会导致内存空间不足或者程序性能下降的问题。

原文: https://www.yuque.com/hollis666/xkm7k3/zcd5ur