Java GC之对象已死吗
差不多两年以前曾经写过一篇文章:JAVA 性能调优,其实在那篇文章中只是简单的说了,对象的分布。这篇文章继续对分布于堆中的对象的生命周期进行说明,也就是确定堆中的这些对象哪些还是“活着”的,哪些是已经“死去”(即不可能再被任何途径使用的对象)的。
1. 引用计数算法
有很多人认为判断对象是否活着的算法是这样的:给对象添加一个引用计数器,每当有一个地方引用他的时候,计数器就加1,引用失效的时候,计数器减1,当计数器的数值为0时就是不可能在被引用的对象,此时就就可以认为是已死的对象。引用计数器算法实现简单,效率也很高,是一个不错的算法,但是主流的Java虚拟机并没有采用这种算法来管理内存,其中最主要的原因就是:它很难解决对象之间循环引用的问题。
举一个简单的例子:对象objA和objB都有字段instance,赋值令,除此之外,这两个对象再无任何引用,实际上他们已经不可能在被访问到,但是他们因为相互引用对方,计数器都不可能为0,计数器算法是无法通知GC收集器收集他们的。
package demo; /** * testGC()方法执行后,objA和objB会不会被GC呢? * * @author BridgeLi * */ public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; // 这个成员的唯一意义就是占用内存,以便能在GC日志中看清楚是否被回收过 private byte[] bigSize = new byte[2 * _1MB]; public static void testGC() { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假设发生了GC,看objA和objB是否能被回收 System.gc(); } public static void main(String[] args) { ReferenceCountingGC.testGC(); } }
从这个例子的运行结果来看,虚拟机并没有这两个对象存在相互引用就不收集他们,从而证明了Java虚拟机不是通过引用计数算法来判断对象是否已死的。
2. 可达性分析算法
该算法的基本思路就是通过一系列成为“GC Roots”的对象作为起始点,从这些起点向下搜索,搜索所走过的路径称为引用链(Reference chain),当一个对象到GC Roots没有任何引用链相连时,则此对象就是不可用的。在Java语言中,可作为GC Roots的对象包括下面几种:
①. 虚拟机栈中引用的(栈帧中本地变量表)对象
②. 方法区中类静态属性引用的对象
③. 方法区中常量引用的对象
④. 本地方法栈中JNI引用的对象
可达性分析算法的示意图,如下:
3. 生存还是死亡
其实即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这个时候他们处于“缓刑”阶段,至少要经历两次标记过程:如果对象在进行可达性分析后没有与GC Roots相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象有无必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()已经被虚拟机执行过,将被视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么该对象将会被放置在一个叫做F-Quene的队列之中,稍后虚拟机会自动建立一个低优先级的Finzlizer线程去执行他。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待他运行结束,这么做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很有可能会导致F-Quene队列中的其他对象永远处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Quene中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己–只要重新与引用链上的任何一个对象管理上即可,那么第二次标记时他将被移出“即将回收”的集合,如果对象在这个时候还没有逃脱,那他就真的死了
package demo; /** * 此代码演示了两点: 1. 对象可以在被GC时自我拯救。 2. 这种自我拯救机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 * * @author BridgeLi * */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, I am still alive"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); // 对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因为finalize()方法优先级很低,所以暂停0.5秒以等待他 Thread.sleep(500); if (null != SAVE_HOOK) { SAVE_HOOK.isAlive(); } else { System.out.println("no, I am dead"); } // 这段代码和上面的代码相同,但是这次自我拯救却失败了 SAVE_HOOK = null; System.gc(); // 因为finalize()方法优先级很低,所以暂停0.5秒以等待他 Thread.sleep(500); if (null != SAVE_HOOK) { SAVE_HOOK.isAlive(); } else { System.out.println("no, I am dead"); } } }
需要说明的是,大家尽量不要使用这种方法来拯救对象,这只是Java刚诞生时为了是c和c++程序猿更容易接受而做的一个妥协,他的运行代价高,不确定性大,无法保证各个对象的调用顺序。
参考资料:周志明《深入理解Java虚拟机》第二版第三章
作 者: BridgeLi,https://www.bridgeli.cn
原文链接:http://www.bridgeli.cn/archives/330
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
近期评论