Java对象的"后事处理"——垃圾回收(一)
1、Dead Or Alive
如上图中,栈中没有任何堆中两个对象的引用,而堆中的两个对象则互相持有对方的引用,如果使用引用计数法的话引用变量值永远不会为0,从而造成内存泄漏,两个互相引用的对象无法释放空间。
public class TestForGc { TestForGc testInstance; // 模拟上图的现象 public static void main(String[] args) { TestForGc testA = new TestForGc(); TestForGc testB = new TestForGc(); testA.testInstance = testB; testB.testInstance = testA; testA = null; testB = null; // 建议垃圾回收器进行回收操作 System.gc(); } }
然后设置-XX:+PrintGCDetails打印GC日志:
最终新生代的对象全部被回收,说明JVM使用的并不是使用引用计数法来实现垃圾回收。
1.2 可达性分析算法(GCRoots)
在Hotspot虚拟机对GCRoots算法的实现中,大致可以分为三个部分理解。
1.2.1 枚举根节点#
如上所说,根节点的选取对象有四处,如果虚拟机对这些位置进行全盘扫描的话,效率自然要影响不少,所以Hotspot采用一种数据结构来解决这个问题——OopMap。在类加载完成的时候,虚拟机将对象的什么偏移量有什么对象计算出来,在JIT编译过程中在特定的位置记录下栈和寄存器中哪些位置是引用。这样一来GC在扫描的时候就可以直接得到这些引用的信息,从而减少GC的停顿时间。顺便一提,在枚举根节点的时候,为了保持“一致性”,不能再扫描的时候还出现对象引用变化的情况,所以需要暂停所有Java执行线程(被称为"STOP-THE-WORLD"),即便在具有划时代意义、可以并发执行的CMS收集器中在枚举根节点的时候也需要STW。
1.2.2 安全点的设置#
OopMap数据结构可以说为GC的扫描减少了不少的时间,但是随之而来的还有一个问题,如果每条指令都生成对应的OopMap,那么想必需要大量的额外空间,GC的空间成本将十分巨大,就是何时生成对应OopMap成为当前面临的问题。之前说过在特定的位置会记录下引用的位置,这个特定的位置就是OopMap的生成时机,也就是“安全点(SafePoint)”,在Sop-The-World的时候线程要先跑到安全点才可以进行线程的停顿。那该如何判断这个特定的位置呢?如果设置的太少可能会导致GC时间变长,设置的太多会增大运行时的负荷。Hotspot给出的答案是以程序“是否具有让程序长时间执行的特征”为标准进行选定。"长时间执行"的明显特征就是指令复用,例如方法调用、循环跳转、异常处理等,只有这些指令才能产生安全点。
对于安全点来说,另外一个问题就是采用什么样的方式让所有的线程跑到最近的安全点停顿。有两种实现的方式:
1、抢先式中断:在GC发生的时候首先暂停所有线程,如果发现有线程没在安全点的话,则恢复线程,让其跑到最近的安全点再进行暂停。现在已经很少有使用抢先式的了。
2、主动式中断:GC发生的时候不强制暂停线程,而是设置一个标识变量,线程会去轮询这个标志,如果为true则将自己中断挂起。这个轮询的位置和安全点是重合的,还有创建对象时需要分配内存的地方。
1.2.3 安全区域#
上面安全点的设置几乎已经解决了问题,但是还少了一点,就是建立在线程都是执行状态的时候,那线程不执行的时候呢,例如进入休眠状态的线程,这时候自己不能跑到安全点也不能等待JVM分配时间。此时就需要安全区域来解决这一点。
安全区域指的是在一段代码块中,引用关系不会发生变化。当程序走到安全区域的时候,则标识当前线程进入了安全区域。这时候发生GC的时候则可以不用管有安全区域标识的线程,而这些线程在快离开安全区域的时候必须要检查是否完成了根节点的枚举(或者整个GC的过程),如果完成了才可以离开安全区域,否则必须待到完成为止。
2、垃圾回收算法#
现在我们知道哪些对象是死亡的,哪些对象应该回收,而这个回收有许多种实现的方式(算法),有的算法对死亡对象进行标记最后一并清除、有的算法将内存分块然后将存活对象从一头搬到另一头,还有算法在清除完死亡对象贴心的将存活的对象整放在一块儿,这些都是我们接下来要说的。
2.1 标记-清除算法#
正如这个算法的名称一般,其总共有两个阶段——"