[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 | |
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 | 全局静态变量,是一个引用队列,保存Finalizer对象 |
Finalizer unfinalized | 全局静态变量,Finalizer对象以双向链表的形式保存,该变量指向当前链表的头部,每次新创建一个Finalizer对象会将其放在链表头部 |
Object lock | 全局静态变量,用于在对双向链表操作时加锁 |
next | 链表中当前对象的下一个 |
prev | 链表中当前对象的前一个 |
注意到Finalizer类的源码中有一个register方法,注释是该方法由VM调用,推测是当重写了finalize()的对象初始化时会调用register方法(推荐文章中有对这部分的hotspot源码分析),该方法会将该对象作为构造器参数创建一个Finalizer对象,Finalizer继承了FinalReference
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
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 | |
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。