前言

深入浅出Java虚拟机(三)——初识JVM的垃圾回收机制 中我们介绍了如何判断对象已死的两种算法,分别是引用计数法和可达性分析法。

本文我们再来详细说明一下JVM中判断对象是否、存活的相关细节。

finalize()方法的执行条件

即使在可达性分析算法中判定为不可达的对象, 也不是“非死不可”的, 这时候它们暂时还处于“缓刑”阶段, 要真正宣告一个对象死亡, 至少要经历两次标记过程:

第一次标记

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记, 随后进行一次筛 选, 筛选的条件是此对象是否有必要执行**finalize()**方法。 没有必要: 假如对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过, 那么虚拟机将这两种情况都视为“没有必要执行”。

有必要

如果这个对象被判定为确有必要执行finalize()方法, 那么该对象将会被放置在一个名为F-Queue的 队列之中, 并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的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
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次, 因为一个对象的finalize()方法最多只会被系统自动调用一次
*/
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(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else {
System.out.println("no i am dead");
}


SAVE_HOOK=null;
System.gc();
Thread.sleep(500);

if(SAVE_HOOK!=null){
SAVE_HOOK.isAlive();
}else {
System.out.println("no i am dead");
}
}
}

注意: Finalizer线程去执行它们的finalize() 方法, 这里所说的“执行”是指虚拟机会触发这个方法开始运行, 但并不承诺一定会等待它运行结束。 这样做的原因是, 如果某个对象的finalize()方法执行缓慢, 或者更极端地发生了死循环, 将很可能导致F-Queue队列中的其他对象永久处于等待, 甚至导致整个内存回收子系统的崩溃。

再谈引用

深入浅出Java虚拟机(三)——初识JVM的垃圾回收机制 中我们有说到几种引用类型。这里我们再复习一下。

在JDK1.2以前,Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就 称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。 我们希望能描述这一类对象: 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。 在JDK1.2之后,Java对引用的概念做了扩充,将引用分为 强引用(Strong Reference) 、 软引用(Soft Reference) 、 弱引用(Weak Reference) 和 虚引 用(Phantom Reference) 四种,这四种引用的强度依次递减。

⑴强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟 机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。

⑵软引用(SoxReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对 象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可以和一个引用队列 (ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

⑶弱引用(WeakReference)

用来描述那些非必须对象, 但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发 生为止。 当垃圾收集器开始工作, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象。 在JDK 1.2版之 后提供了WeakReference类来实现弱引用。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

弱引用与软引用的区别在于: ①更短暂的生命周期; ②一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

⑷虚引用(PhantomReference)

“虚引用”顾名思义,它是最弱的一种引用关系。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收器回 收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

虚引用与软引用和弱引用的一个区别在于: ①虚引用必须和引用队列 (ReferenceQueue)联合使用。 ②当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到 与之关联的引用队列中。

总结

我们认识了finalize()方法的执行条件以及如何通过finalize()方法中对对象进行自救。又介绍了下几种引用的情况。