对象引用与回收

在之前的文章中学习了 JVM 内存模型,程序寄存器、虚拟机栈、本地方法栈都是线程私有的,在内存管理上当线程结束时资源就会释放掉。这几个区域内存的分配和类结构相关,大概在编译器就可以决定,所以他们的内存分配和回收都具备确定性。但是堆和方法区是线程共享内存区域,线程结束内存不一定被回收,每个接口的实现类需要的内存不一样、方法运行期间才知道需要创建那些对象,这部分内存的分配和回收都是动态的,这也是垃圾收集(GC)器工作的主要对象。

那些对象需要被回收

Java 中的对象都是在堆上分配内存空间的,然后通过引用指向对象地址。是否被引用是判断对象是否还存活的重要根据。

对象引用

JDK 1.2之前,reference 数据类型存储的数值代表一个内存地址表示着一个引用。随着硬件的发展内存空间虽然很珍贵但是还可满足,所以在内存空间充足的情况下,有些对象则尽可能的保留在内存中当内存不足时再回收。如何区分这些对象呢,就是通过引用的强弱关系来区分,主要有四种:

  • 强引用(Strong Reference):这是在代码中普遍存在的一种引用,类似:Object obj = new Object(),如果强引用还存在则一定不会被回收。
  • 软引用(Soft Reference):通过 SoftReference 类来实现,表示有用但是非必需的对象。系统发生内存溢出之前(已经 GC 一次)会把软引用关联的对象列进回收范围进行第二次回收,如果回收后还是内存不足则会抛出内存溢出异常。
  • 弱引用(Weak Reference):有用非必需对象,通过 WeakReference 类来实现,弱引用对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时都会回收掉弱引用关联的对象。
  • 虚引用(Phantom Reference):最弱的引用关系,虚引用不会对对象的存活造成影响,无法通过虚引用在获取一个对象实例。为一个对象设置虚引用是在对象被垃圾回收时可以收到一个系统的通知。可以通过 PhantomReference 类来实现。

对象是否还被引用,主要通过下面两种算法来判断:

引用计数法

给对象添加一个引用计数器,当对象被引用的时候计数器就增加 1,引用失效时计数器减 1,任何时刻计数器为 0 的对象就是不再被使用的。但是当对象存在循环引用时,其实环上的对象都已经不在被使用,但是计数器不为 0,这样会造成对象无法被回收。

可达性分析算法

通过 GC Roots 的对象作为起点,对引用到的对象构造一条引用链,当某个对象没有到达 GC Roots 的路径,称为不可达。这部分对象被判定为可回收的。Java 虚拟机中可以作为 GC Roots 对象的有:

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

对象的真正死亡也至少要经过两个标记过程:

第一次标记

可达性分析后对象没有与 GC Roots 相连的引用链,此时进行第一次标记并且进行一次筛选。筛选条件是次对象是否需要执行 finalize() 方法。如果对象没有覆盖 finalize 方法、或者finalize 方法已经被虚拟机调用过,这两种情况视为不需要执行。

如果对象需要执行 finalize 方法,对象会被放在 F-Queue 队列中,稍后由虚拟机自动建立的、低优先级的 Finalizer 线程去执行。这个执行仅是触发,不会等到方法执行完,避免出现 finalize 方法执行慢、死循环等情况使 F-Queue 对垒处于等待最终导致回收系统奔溃。第一次标记后的 finalize 方法是对象逃脱死亡的最后机会。

第二次标记

第二次标记是针对在 F-Queue 队列中的对象,此时如果对象确实没有引用了就会被回收。所以在第一次的标记后的 finalize 方法执行时讲对象重新与引用链上对象建立引用关系即可被移出即将回收的集合。这种自救的方法只有一次,因为对象的 finalize 方法只会被虚拟机执行一次。

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 FinalizeEscapeGc {

public static FinalizeEscapeGc SAVE_HOOK = null;

public void isAlive() {
System.out.println("对象还存活");
}

@Override
protected void finalize() throws Throwable {
System.out.println("finalize 方法开始执行");
super.finalize();
FinalizeEscapeGc.SAVE_HOOK = this;
}

public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGc();

//第一次的回收时,通过指向this,拯救自己
SAVE_HOOK = null;
System.gc();
//finalize 方法的优先级较低,线程停500ms等待
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("对象已死");
}

//第二次的回收时,回收成功;因为finalize方法只能被虚拟机执行一次,所以这次会被回收掉
SAVE_HOOK = null;
System.gc();
//finalize 方法的优先级较低,线程停500ms等待
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("对象已死");
}
}
}

//finalize 方法开始执行
//对象还存活
//对象已死
//输出结果可看到,finalize只被执行了一次,对象第一次还存活第二次被回收了。

常量、类回收

上面主要说的是堆上对象的回收,对于方法区或者永久代虽然回收的效率比较低但是也存在垃圾回收。永久代主要回收两部分:废弃的常量和无用的类。

废弃常量

如果常量池中的常量没有其他对象引用了。如果此时发生内存回收并且内存可能已经不足这个常量就会被清除。常量池中的其他类(接口)、方法、字段等符号引用也类似。

无用的类

一个类是否无用的判断则比较苛刻:

  • 该类的所有实例都被回收,即堆上不存在该类的实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对象的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足这些条件时才有可能被回收,是否回收虚拟机提供了 -Xnoclassgc 参数来控制。