finalize

前言

在总结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;
        }
    }

接着我们看代码执行过程:

  1. 主线程调用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()方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值