前言
在总结Java中的4种引用的时候,在查找资料的时候,看到了有关于Object#finalize()
方法的一些介绍,提到我们不应该在程序中使用finalize()
方法,该方法不安全,低效。但是并没有给出具体的原因,那今天就结合网上的资料和自己的理解来分析这个finalize()
方法。
为什么不要使用finalize方法
我们来看一个简单的测试
/*
* 虚引用,只有当对象被GC销毁时,才会加入到ReferenceQueue
*
* 因为重写了finalize,导致了对象要通过两次GC才会被回收。 所以也就有虚引用需要GC两次才能在ReferenceQueue中看到他的身影
*
*/
@Test
public void testPhantom() {
ReferenceQueue<Object> rq = new ReferenceQueue<>();
PhantomReference<Object> pr = new PhantomReference<>(new Object() {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize");
}
}, rq);
System.out.println(pr + ", " + pr.get() + ", " + rq.poll());
//System.gc()告诉JVM这是一个执行GC的好时机,但具体执不执行由JVM决定(事实上这段代码一般都会执行GC)
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(pr + ", " + pr.get() + ", " + rq.poll());
}
我们调用了两次GC方法,输出如下
java.lang.ref.PhantomReference@3b22cdd0, null, null
finalize
java.lang.ref.PhantomReference@3b22cdd0, null, java.lang.ref.PhantomReference@3b22cdd0
如果这里只调用一次System.gc();
,输出如下
java.lang.ref.PhantomReference@3b22cdd0, null, null
finalize
java.lang.ref.PhantomReference@3b22cdd0, null, null
如果将重写finalize()
这块代码注释掉,输出如下
java.lang.ref.PhantomReference@3b22cdd0, null, null
java.lang.ref.PhantomReference@3b22cdd0, null, java.lang.ref.PhantomReference@3b22cdd0
首先,我们要明确一点,对于虚引用,只有当对象被GC销毁时,才会加入到ReferenceQueue。而对于重写了finalize()
的虚引用对象,我们必须要调用两次GC才能使得虚引用加入到ReferenceQueue,也就证明了重写了finalize方法的对象,会导致至少两次GC才会被回收。
那么这是为什么呢?来分析下,当一个类重写了finalize()
方法后,我们在生成一个该类的对象的时候,会同时生成一个java.lang.ref.Finalizer
对象,指向该对象。
Finalizer内部维护了一个unfinalized链表,每次创建的Finalizer对象都会插入到该链表中
private static final Object lock = new Object();
private Finalizer
next = null,
prev = null;
private boolean hasBeenFinalized() {
return (next == this);
}
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
接着我们看代码执行过程:
-
主线程调用
System.gc();
,建议触发GC,一般情况下都会触发GC,这里就假定GC一定发生。2.接下来Minor GC回收器发出Stop The World指令,主线程被挂起。开始Mark-Copy算法,标记堆中所有unreachable对象。
3.如果某个unreachable对象是finalizable(即有一个Finalizer对象指向他),Minor GC知道不能马上回收掉,需要先执行finalize()方法。但是finalize()方法Minor GC自己又不能指向。需要Finalizer的finalize daemon thread(java.lang.ref.Finalizer$FinalizerThread)线程负责执行。所以Minor GC只要先把他插入到finalization queue(F-queue),并从unfinalized链表中删除该结点。等到以后finalize daemon thread可以执行的时候,会依次执行队列里对象的finalize()方法。
4.当对象被插入到F-queue之后,这个对象就会被Finalizer class所引用,会被标记会reachable,所以这第一轮的GC过程中,该对象没有被回收,从Eden被拷贝到了SUrvivor区。
5.当第一轮GC结束后,主线程继续原先,因为FinalizerThread优先级比主线程低很多,会在某个不确定的时候执行finalize()方法,然后对象被标记为finalized。这时候对象和Finalizer class之间的强引用才断开,对象重新变为unreachalbe。
6.如果执行完finalize()方法后,又进行了一次GC,这个时候就会把对象回收,对象回收之后,才会加入到ReferenceQueue。
总结
我们知道了对于一个实现了finalize()方法的类,在创建该类的一个对象的时候,会跟着创建一个java.lang.ref.Finalizer
对象,要通过两次GC才能回收掉该对象。于是,我们可以得到如下结论:
-
使用finalize会导致更多的内存消耗和性能损失
首先创建对象时,会多创建一个
java.lang.ref.Finalizer
对象,而回收的时候需要两次GC才能回收的掉,而且执行finalize()方法的java.lang.ref.Finalizer$FinalizerThrea
线程的优先级很低,这就导致了可能存在大量的无用的Finalizer对象的存在,由于需要两次GC,导致垃圾回收的速度变慢了,对象的销毁速度小于对象的创建速度。所以千万不要使用finalize()
方法。