Java虚拟机学习笔记-判断对象是否存活

写在前面

  记录一下,在垃圾收集器对堆进行回收前,确定对象是否“存活”的基本步骤或者算法,以及对象的自我拯救和引用的相关概念。


引用计数算法

  引用计数算法(Reference Counting),简单来说,就是给对象中添加一个引用计数器,有一个对象引用它的时候,计数器值就加1;引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被引用的。

  但是,目前主流的Java虚拟机里面没有选用引用计数算法,其中最主要的原因是它很难解决对象之间相互循环引用的问题。


可达性分析算法

  Java的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。它的基本思路是通过一系列叫做“GC Roots”的对象作为起始点,向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,或者说从GC Roots到这个对象不可达时,则证明此对象是不可用的。如下图所示,对象object 5object 6object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们被判定为是可回收对象。

可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

四类引用

  无论是通过引用计数算法判断对象的引用对象,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。

  在JDK 1.2之后,Java对引用的概念进行了扩充,分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,引用强度依次逐渐减弱。

  • 强引用类似于“Object obj = new Objcet()”这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象
  • 软引用用来描述非必需对象的。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类才实现软引用
  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用
  • 虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2后,提供了PhantomReference类来实现虚引用。

对象的自我拯救

  在可达性分析算法中被标记为不可达对象,不是必须要回收的。要真正宣告一个对象死亡,至少要经过两次标记过程:经过可达性分析被第一次标记后,要进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则视为没有必要执行。

  如果此对象被判定为有必要执行finalize()方法的话,那么对象想成功拯救自己的话,就需要在finalize()方法中重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合。当然,如果对象这时候还没有逃脱,就基本上要被回收了。

 以下是《深入理解Java虚拟机》中提供的代码,方便理解对象自我拯救的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统调用一次
*
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;

public void isALive() {
System.out.println("我还活着。。。");
}

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("执行finalize方法");
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 (SAVE_HOOK != null) {
SAVE_HOOK.isALive();
} else {
System.out.println("不,我怎么死了");
}

//下面的代码与上面的完全相同,但是这次自救失败了
//因为finalize()已经在上面被调用过了
SAVE_HOOK = null;
System.gc();
//因为finalize优先级很低,所以暂停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isALive();
} else {
System.out.println("不,我怎么死了");
}
}
}

以上代码运行输出如下:

1
2
3
执行finalize方法
我还活着。。。
不,我怎么死了

小结

  简单记录一下,加深印象,方便以后查看。


参考资料

Author: HowieLi
Link: https://www.howieli.cn/posts/java-instance-whether-survival.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.