0x00 前言
GC的第一步就是要判断出哪些对象需要被回收。显然易见的是,当一个对象不再被使用后,那么就需要对其进行回收。那么问题就是,如何判断对象是否被使用?本文将介绍两种算法来判断对象的使用情况。
0x01 引用计数
简而言之,引用计数就是为每一个对象增加一个计数器,当对象被引用时,计数器加一,而当对象的引用消失时,计数器减一。在每一轮GC开始时,扫描堆中所有对象的计数器,并回收那些计数为0的对象。
引用计数的优点是实现简单,但是它有十分明显的缺点:
- 计数器的并发读写。当对象被多个线程共享时,那么每一次修改计数值都需要使用锁或者其它机制防止出现并发问题,当对象被频繁访问时这是很大一部分的性能开销。
- 循环引用。考虑这样一个场景,对象A引用了对象B,而对象B又引用了对象A,那么即使其它所有对象都解除了关于对象A、B的引用,它们的计数器仍然保留为1,因此永远不会被GC回收。
0x02 可达性分析
Java使用可达性分析来判断对象时候需要被回收。所谓可达性,指的是从根对象(GC root)出发,如果可以沿着引用链找到该对象,则称该对象是可达的,否则就是不可达的,需要被回收。而根对象则会一直保留,不会被GC回收。
那么哪些对象属于根对象呢?主要分为以下四类:
- Thread类关联的对象。当线程在执行时,其当前栈帧上的局部对象不会被GC回收,由这些局部对象出发,可以找到被引用的对象以及不可达的对象。
- Class对象。Class对象包含了一些类的静态对象,这些对象同样作为静态对象,并且由此产生了一颗对象树,在这颗树上的所有对象是可达的。
- 监视器对象。synchronized关键字持有的对象,这些对象产生线程竞争时需要的锁,同样也不可以被回收。
- JNI引用的对象。这部分由JVM来管理,作为应用程序员不用去关注。
0x03 总结
引用计数算法虽然简单,但是无法解决循环依赖的问题,因此大部分场景下不会使用它。Java中选择使用可达性分析来判断对象是否需要被回收,其优点在于较高的回收性能以及不存在循环引用问题。但是可达性分析仍然存在问题,因为在程序运行过程中对象的引用关系是不断发生变化的,那么在进行可达性分析时如果不暂停程序,就可能出现错误的引用关系判断,而一旦长时间地暂停程序,则造成卡顿问题。这个问题是否无解呢?