如何判断对象是否可以回收

如何判断Java对象是否可以回收

Java中垃圾回收是负责税收已经消亡或者是不再使用的对象,那么如何判断对象是否可以回收呢?

主流的方法有两种:引用计数可达性分析

引用计数

引用计数的算法流程是这样的:在对象中添加一个引用计数器,一旦有一个新的引用指向这个对象,它的计数器值就加一;当引用失效或者指向别处时,计数器数值就减一;当这个计数器的值为零时,就代表着这个对象可以回收了。

Python语言就是用的这个方法来判断对象是否可以回收的。

但是,这个方法有一个比较致命的缺点,就是它解决不了循环引用的问题。

看下面这个代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
Test test;

public Test() {

}

public void setTest(Test other) {
this.test = other;
}

public static void main(String[] args) {
Test t1 = new Test();
Test t2 = new Test();

t1.setTest(t2);
t2.setTest(t1);
}
}

对象t1指向了t2,对象t2又指向了t1。这样就会导致他们的引用计数都是1.就算后面这两个对象没有在使用,因为它们互相引用的关系,它们是不会被垃圾回收的。就会造成内存泄露问题。

可达性分析算法

可达性分析算法的基本思想是:通过一些列被称为“GC Roots”的根对象,作为起始节点集,从这些节点开始,根据引用对象向下搜索,搜索过程所走过的路径称为”引用链“,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

如下图

可达性分析-标记对象

在上图中,ObE和ObjD就是不可达的对象,它们可以直接被回收掉。

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用
  • 在本地方法栈JNI(即通常所说的Native方法)引用的对象
  • Java虚拟机内部的引用,如基本熟路类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryErrot)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反应Java虚拟机内部情况的JMXBean。JVMTI中注册的回调、本地代码缓存等。

对象被标记回收之后,如何逃过垃圾回收

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的。要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么它将会被第一次标记,随后进行一次筛选,筛选的条件时此对象是否有必要执行finalize()方法。加入对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。

但是,这里虚拟机会触发执行finalize()方法,但是不承诺会等这个方法执行结束。因为存在某个对象的finalize()方法执行缓慢,或者陷入死循环导致其他对象一直处于等待状态。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要冲印与引用链上的任何一个对象建立关联即可。譬如,把自己(this关键字)赋值给某个类变量或者对象的成员变脸,那么在第二次标记时它将被移出“即将回收”的集合;如果这个对象这时候还没有逃脱,那基本上他就真的要被回收了。

以下面这个代码段为例:

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
public class Test {
public static Test SAVE_HOOK = null;

public void isAlive() {
System.out.println("yes, i am alive");
}

@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
super.finalize();

System.out.println("finalize method execute");
// 将SAVE_HOOK重新链上存活对象
SAVE_HOOK = this;
}

public static void main(String[] args) throws InterruptedException {
// 初始化 SAVE_HOOK
SAVE_HOOK = new Test();

// 将SAVE_HOOK设置为null,以便可以垃圾回收
SAVE_HOOK = null;
// 触发垃圾回收
System.gc();

// 暂停,因为finalizer线程优先级低
Thread.sleep(500);
if (null != SAVE_HOOK) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No i am dead.");
}

// 同上面一样的代码,但是因为finalize方法只执行一次
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (null != SAVE_HOOK) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No i am dead.");
}
}
}

执行结果

对象逃脱垃圾回收

回收方法区

方法区的垃圾回收主要回收两部分内容:废弃的常量和不再使用的类型。

没有任何一个对象引用某个常量的话,该常量就可以被回收了。

而判定一个类型是否属于“不再使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

参考资料

[深入理解Java虚拟机]

0%