0%

Object.finalize()方法与Finalizer类浅析

[TOC]

Object.finalize()方法与Finalizer类浅析

这部分内容属于GC相关的内容,可以参考 https://www.jianshu.com/p/9d2788fffd5f ^2,此文写的比较深入,涉及到了hotspot的源码,强烈推荐。

一、finalize()^1

1
public class FinalizerDemo {
2
3
    static AtomicInteger aliveCount = new AtomicInteger(0);
4
5
    FinalizerDemo() {
6
        aliveCount.incrementAndGet();
7
    }
8
9
    @Override
10
    protected void finalize() throws Throwable {
11
        FinalizerDemo.aliveCount.decrementAndGet();
12
    }
13
14
    public static void main(String args[]) {
15
        for (int i = 0; ; i++) {
16
            FinalizerDemo f = new FinalizerDemo();
17
            if ((i % 100_000) == 0) {
18
                System.out.format("After creating %d objects, %d are still alive.%n", new Object[]{i, FinalizerDemo.aliveCount.get()});
19
            }
20
        }
21
    }
22
}

运行这段代码,加上-XX:+PrintGCDetails参数查看GC详情,可以发现很快就开始full gc,而去掉对finalize()方法的重写则不会触发full gc。这是为什么呢?这就涉及到重写了finalize()方法的类的回收了,下面详细说一下,或者也可以直接阅读上面推荐的文章。

列一下finalize()方法相关的特性:

  • 当GC判断没有其他对该对象的引用时,会调用该方法,一般用于执行资源释放或其他清理工作
  • finalize()方法抛出的异常会被忽略
  • finalize()方法中可以使该对象重新关联到引用链上,也即是“复活”该对象,例如,让一个静态全局变量指向该对象,使其免于被回收
  • finalize()方法最多执行一次

二、Finalizer

阅读上面推荐的文章就知道finalize()方法与Finalizer类脱不开关系。下面分析一下这个类的源码。

Finalizer类有3个静态变量,2个成员变量,分别是:

变量 作用
ReferenceQueue queue 全局静态变量,是一个引用队列,保存Finalizer对象
Finalizer unfinalized 全局静态变量,Finalizer对象以双向链表的形式保存,该变量指向当前链表的头部,每次新创建一个Finalizer对象会将其放在链表头部
Object lock 全局静态变量,用于在对双向链表操作时加锁
next 链表中当前对象的下一个
prev 链表中当前对象的前一个

注意到Finalizer类的源码中有一个register方法,注释是该方法由VM调用,推测是当重写了finalize()的对象初始化时会调用register方法(推荐文章中有对这部分的hotspot源码分析),该方法会将该对象作为构造器参数创建一个Finalizer对象,Finalizer继承了FinalReference,因此构造器会将finalizee(即实现了finalize()方法的对象)和全局的ReferenceQueue作为构造参数构造Reference

1
private Finalizer(Object finalizee) {
2
    super(finalizee, queue);
3
    add();
4
}
5
6
/* Invoked by VM */
7
static void register(Object finalizee) {
8
    new Finalizer(finalizee);
9
}

Finalizer类的runFinalizer方法就是最终用来调用对象的finalize()方法的。先判断是否已执行过finalize()方法,若没有,则从双向链表中移除自身,然后调用finalizee的finalize()方法,最后finalizee = null;这一步注释说的是清理包含这个变量的栈帧,减少false retention概率,关于false retention后面单开文章说。

1
private void runFinalizer(JavaLangAccess jla) {
2
        synchronized (this) {
3
            if (hasBeenFinalized()) return;
4
            remove();//不去掉的话,会导致对象仍然在引用链上,无法回收
5
        }
6
        try {
7
            Object finalizee = this.get();
8
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
9
                jla.invokeFinalize(finalizee);
10
11
                /* Clear stack slot containing this variable, to decrease
12
                   the chances of false retention with a conservative GC */
13
                finalizee = null;
14
            }
15
        } catch (Throwable x) { }
16
        super.clear();
17
    }

FinalizerThread

Finalizer类通过静态初始化代码块启动一个FinalizerThread守护线程,该线程优先级较低。注意线程优先级较低这一点,正是这一点导致在上面的示例中来不及执行对象的finalize()方法,导致垃圾回收很慢,内存泄漏,触发full gc,甚至OOM。

1
static {
2
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
3
    for (ThreadGroup tgn = tg;
4
         tgn != null;
5
         tg = tgn, tgn = tg.getParent());
6
    Thread finalizer = new FinalizerThread(tg);
7
    finalizer.setPriority(Thread.MAX_PRIORITY - 2);
8
    finalizer.setDaemon(true);
9
    finalizer.start();
10
}

下面看下FinalizerThread的逻辑:

1
private static class FinalizerThread extends Thread {
2
    private volatile boolean running;
3
    FinalizerThread(ThreadGroup g) {
4
        super(g, "Finalizer");
5
    }
6
    public void run() {
7
        if (running)
8
            return;
9
10
        // Finalizer thread starts before System.initializeSystemClass
11
        // is called.  Wait until JavaLangAccess is available
12
        while (!VM.isBooted()) {
13
            // delay until VM completes initialization
14
            try {
15
                VM.awaitBooted();
16
            } catch (InterruptedException x) {
17
                // ignore and continue
18
            }
19
        }
20
        final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
21
        running = true;
22
        for (;;) {
23
            try {
24
                Finalizer f = (Finalizer)queue.remove();
25
                f.runFinalizer(jla);
26
            } catch (InterruptedException x) {
27
                // ignore and continue
28
            }
29
        }
30
    }
31
}

主要关注最后的死循环部分,调用queue的remove()方法,取出队列中的Finalizer对象,执行其runFinalizer方法。

到这里就剩下一个疑问点,Finalizer对象是什么时候加入queue队列的?这就要说到Reference类了。

三、Reference

前面说到Finalizer继承了FinalReference,而FinalReference类继承了Reference。看一下这个类的静态初始化代码块,里面启动了一个ReferenceHandler守护线程,该线程优先级较高。

ReferenceHandler

1
static {
2
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
3
    for (ThreadGroup tgn = tg;
4
         tgn != null;
5
         tg = tgn, tgn = tg.getParent());
6
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
7
    /* If there were a special system-only priority greater than
8
     * MAX_PRIORITY, it would be used here
9
     */
10
    handler.setPriority(Thread.MAX_PRIORITY);
11
    handler.setDaemon(true);
12
    handler.start();
13
14
    // provide access in SharedSecrets
15
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
16
        @Override
17
        public boolean tryHandlePendingReference() {
18
            return tryHandlePending(false);
19
        }
20
    });
21
}

ReferenceHandle线程死循环执行一个方法tryHandlePending,源码如下:

1
static boolean tryHandlePending(boolean waitForNotify) {
2
        Reference<Object> r;
3
        Cleaner c;
4
        try {
5
            synchronized (lock) {
6
                if (pending != null) {
7
                    r = pending;
8
                    // 'instanceof' might throw OutOfMemoryError sometimes
9
                    // so do this before un-linking 'r' from the 'pending' chain...
10
                    c = r instanceof Cleaner ? (Cleaner) r : null;
11
                    // unlink 'r' from 'pending' chain
12
                    pending = r.discovered;
13
                    r.discovered = null;
14
                } else {
15
                    // The waiting on the lock may cause an OutOfMemoryError
16
                    // because it may try to allocate exception objects.
17
                    if (waitForNotify) {
18
                        lock.wait();
19
                    }
20
                    // retry if waited
21
                    return waitForNotify;
22
                }
23
            }
24
        } catch (OutOfMemoryError x) {
25
            // Give other threads CPU time so they hopefully drop some live references
26
            // and GC reclaims some space.
27
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
28
            // persistently throws OOME for some time...
29
            Thread.yield();
30
            // retry
31
            return true;
32
        } catch (InterruptedException x) {
33
            // retry
34
            return true;
35
        }
36
37
        // Fast path for cleaners
38
        if (c != null) {
39
            c.clean();
40
            return true;
41
        }
42
43
        ReferenceQueue<? super Object> q = r.queue;
44
        if (q != ReferenceQueue.NULL) q.enqueue(r);
45
        return true;
46
    }

主要逻辑就是将pending链表的头节点取出,并将其加入队列中(最后3行代码),其中pending链表是GC扫描出的pending状态的Reference对象链表,Finalizer实现了Reference,也会被扫描出来,至于具体逻辑就要查看jdk源码了,这个正好在参考文献【2】也就是最开头推荐的文章中有提到。

四、小结

回到最开始的例子,再分析一下。main函数死循环创建一个重写了finalize()方法的对象,JVM会为每一个对象创建一个Finalizer对象,ReferenceHandle线程负责将Finalizer对象加入ReferenceQueue,该线程优先级较高,而FinalizerThread线程负责消费ReferenceQueue,执行对象的finalize()方法,优先级较低。只有当对象的finalize()方法执行完,对象才能被回收。

那么当FinalizerThread线程由于优先级低而抢不到CPU资源,或者finalize()方法执行较慢等原因来不及消费ReferenceQueue时,就会出现GC无法回收垃圾,从而导致full gc。

finalize()方法最多执行一次的原理也在此说一下我个人的理解,不一定正确。上面说到,当对象创建时,如果该对象重写了finalize()方法,JVM会调用Finalizer对象的register方法,即该对象对应的Finalizer对象只会被创建一次,而一旦这个Finalizer对象被移出ReferenceQueue就不会再回到ReferenceQueue。

参考文献