问题
question:当WeakReference的referent重写了finalize方法时会发生什么?
测试代码
JVM中是存在这样的情况的:一个Java对象,重写了finalize方法,在使用的过程中又被SoftReference或WeakReference或PhantomReference封装,这时候JVM是怎么处理该referent的?
软引用受LRU策略的影响,不太好探究。直接使用虚引用在JVM中的处理流程和弱引用一致,但还需要提供一个关系不大的ReferenceQueue,所以选择弱引用来探究这个问题,是更合适的。
用代码来描述这个问题:
public class ThisEscapeAndWeakRef {
private static ThisEscapeAndWeakRef escape = null;
@Override
// finalize逃逸
protected void finalize() throws Throwable {
ThisEscapeAndWeakRef.escape = this;
System.out.println("finalize method running");
}
// 用于测试finalize逃逸是否成功,如果成功escape != null,则可以调用该方法
// 否则报空指针异常
public void isAlive(String step) {
System.out.println(step + ": " + this + " is alive!");
}
public static void main(String[] args) {
// A
escape = new ThisEscapeAndWeakRef();
// B
WeakReference<ThisEscapeAndWeakRef> weakReference = new WeakReference<ThisEscapeAndWeakRef>(escape);
// C
escape = null;
System.gc();
// D
if(escape != null)
escape.isAlive("D");
// E
System.out.println("E: " + weakReference.get());
// F
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// G
if(escape != null)
escape.isAlive("G");
}
}
输出结果:
E: null
finalize method running
G: cn.wxy.gc.ThisEscapeAndWeakRef@5b6f7412 is alive!
分析
我们按照发生时间的先后顺序来分析:
虚拟机启动阶段
- 虚拟机默认参数启动:-XX:+RegisterFinalizersAtInit,表示创建对象在调用构造函数返回时,会将重写了java.lang.Object.fianlzie()方法的对象注册为Finalizer
类加载阶段
- 在虚拟机将ThisEscapeAndWeakRef类在磁盘上对应的class文件加载到持久代或metaspace时,会解析class文件中的方法,发现重写了java.lang.Object.finalize()方法,于是置1该类对应的访问标识位JVM_ACC_HAS_FINALIZER
- 在ThisEscapeAndWeakRef类加载的链接阶段,因为参数RegisterFinalizersAtInit=true并且访问标志JVM_ACC_HAS_FINALIZER置1,所以会将该class文件中原来编译的Object的构造函数中的return指令重写为_return_register_finalizer指令
执行main方法
对应示例代码注释中的每个step:
- A:实例化ThisEscapeAndWeakRef escape,因为在类链接阶段重写了构造函数的return指令,所以通过invokespecial指令调用构造函数返回时重写指令_return_register_finalizer会将escape封装成一个Finalizer
- B:将escape封装成一个WeakReference,escape作为弱引用的referent
现状:此时在JVM中,即存在Finalizer也存在WeakReference,escape对象即是Finalizer的referent,也是WeakReference的referent
- C:escape = null,断开escape的强引用并发起一次显式GC,此时Finalizer和WeakReference都会被GC处理,但是处理顺序无法预测,取决于GC做可达性分析时先扫到谁
流程:
- 对于Finalizer:被GC挂到pending-reference list,随后reference-handler线程取走并放入关联的ReferenceQueue,然后FinalizerThread从ReferenceQueue拿到Finalizer对象,调用Finalizer.get()方法拿到escape,最后调用escape.finalize()并clear referent
- 对于WeakReference:被GC clear referent并挂到pending-reference list,随后reference-handler线程取走,因为没有关联队列,所以流程结束
- D:如果escape逃逸成功,则escape != null,代码执行会调用isAlive方法打印,测试结果该句代码没有执行,所以finalize逃逸还没有成功
- E:通过weakReference.get()拿escape,返回值为null,说明weakReference已经被GC处理,已经被挂到pending-reference list上
- F:因为FinalizerThread不是最高优先级的进程,所以sleep一秒,在此过程中,发现escape.finalize方法在FinalizerThread线程中被调用执行
- G:在F处sleep代码执行的过程中发现escape.finalize方法已经被FinalizerThread调用,再次执行和D处一样的代码,escape.isAlive被调用执行,finalize逃逸成功
结论
一个Java对象,重写了finalize方法,在使用的过程中又被SoftReference/WeakReference/PhantomReference封装:
- 此时在JVM中既有封装了该对象的Finalizer,又有相关的SoftReference/WeakReference/PhantomReference
- 当referent失去强引用时,Finalizer和SoftReference/WeakReference/PhantomReference被GC处理的顺序是不确定的
- A:假设先处理SoftReference/WeakReference/PhantomReference:
- 此时GC会先clear referent,然后再继续Java引用流程
- 等GC开始处理Finalizer时,有且只有Finalizer还持有referent的引用,在调用referent.finalize方法后无论后续是否发生finalize逃逸,referent都已经不再被任何Java引用封装
- B:假设先处理Finalizer:
- Finalizer被GC挂到pending-reference list上时不会clear referent,而稍后FinalizerThread线程进行处理时可能会发生finalize逃逸
- 如果GC处理SoftReference/WeakReference/PhantomReference在finalize逃逸之前,则如同A假设
- 如果GC处理SoftReference/WeakReference/PhantomReference在finalize逃逸之后,此时referent重新变成强可达,所以SoftReference/WeakReference/PhantomReference不会被GC处理,而在Finalizer的流程中已经clear referent,所以此时只有SoftReference/WeakReference/PhantomReference仍旧持有referent的引用
系列目录