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
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
近期评论