写点什么

JVM源码分析之FinalReference完全解读

2015 年 7 月 09 日

概述

Java 对象引用体系除了强引用之外,出于对性能、可扩展性等方面考虑还特地实现了 4 种其他引用:SoftReferenceWeakReferencePhantomReferenceFinalReference,本文主要想讲的是FinalReference,因为当使用内存分析工具,比如 zprofiler、mat 等,分析一些 oom 的 heap 时,经常能看到 java.lang.ref.Finalizer占用的内存大小远远排在前面,而这个类占用的内存大小又和我们这次的主角FinalReference有着密不可分的关系。

FinalReference及关联的内容可能给我们留下如下印象:

  • 自己代码里从没有使用过;
  • 线程 dump 之后,会看到一个叫做Finalizer的 Java 线程;
  • 偶尔能注意到java.lang.ref.Finalizer的存在;
  • 在类里可能会写finalize方法。

FinalReference到底存在的意义是什么,以怎样的形式和我们的代码相关联呢?这是本文要理清的问题。

JDK 中的 FinalReference

首先我们看看FinalReference在 JDK 里的实现:

复制代码
<span><span>class</span> <span>FinalReference</span><<span>T</span>> <span><span>extends</span></span> <span>Reference</span><<span>T</span>> {</span>
<span>public</span> FinalReference(T referent, ReferenceQueue<? <span>super</span> T> q) {
<span>super</span>(referent, q);
}
}

大家应该注意到了类访问权限是 package 的,这也就意味着我们不能直接去对其进行扩展,但是 JDK 里对此类进行了扩展实现java.lang.ref.Finalizer,这个类在概述里提到的过,而此类的访问权限也是 package 的,并且是 final 的,意味着它不能再被扩展了,接下来的重点我们围绕java.lang.ref.Finalizer展开。(PS:后续讲的Finalizer其实也是在说FinalReference。)

复制代码
<span>final</span> <span><span>class</span> <span>Finalizer</span> <span><span>extends</span></span> <span>FinalReference</span> {</span>
<span>static</span> native <span>void</span> invokeFinalizeMethod(Object o) throws Throwable;
<span>private</span> <span>static</span> ReferenceQueue queue = <span>new</span> ReferenceQueue();
<span>private</span> <span>static</span> Finalizer unfinalized = <span>null</span>;
<span>private</span> <span>static</span> <span>final</span> Object lock = <span>new</span> Object();
<span>private</span> Finalizer
next = <span>null</span>,
prev = <span>null</span>;
<span>private</span> Finalizer(Object finalizee) {
<span>super</span>(finalizee, queue);
add();
}
<span>static</span> <span>void</span> register(Object finalizee) {
<span>new</span> Finalizer(finalizee);
}
<span>private</span> <span>void</span> add() {
synchronized (lock) {
<span>if</span> (unfinalized != <span>null</span>) {
<span>this</span>.next = unfinalized;
unfinalized.prev = <span>this</span>;
}
unfinalized = <span>this</span>;
}
}
...
}

Finalizer 的构造函数

Finalizer的构造函数提供了以下几个关键信息:

  • private:意味着我们无法在当前类之外构建这类的对象;
  • finalizee参数:FinalReference指向的对象引用;
  • 调用add方法:将当前对象插入到Finalizer对象链里,链里的对象和Finalizer类静态关联。言外之意是在这个链里的对象都无法被 GC 掉,除非将这种引用关系剥离(因为Finalizer类无法被 unload)。

虽然外面无法创建Finalizer对象,但是它有一个名为register的静态方法,该方法可以创建这种对象,同时将这个对象加入到Finalizer对象链里,这个方法是被 vm 调用的,那么问题来了,vm 在什么情况下会调用这个方法呢?

Finalizer 对象何时被注册到 Finalizer 对象链里

类的修饰有很多,比如 final,abstract,public 等,如果某个类用 final 修饰,我们就说这个类是 final 类,上面列的都是语法层面我们可以显式指定的,在 JVM 里其实还会给类标记一些其他符号,比如finalizer,表示这个类是一个finalizer类(为了和java.lang.ref.Fianlizer类区分,下文在提到的finalizer类时会简称为 f 类),GC 在处理这种类的对象时要做一些特殊的处理,如在这个对象被回收之前会调用它的finalize方法。

如何判断一个类是不是一个 f 类

在讲这个问题之前,我们先来看下java.lang.Object里的一个方法

<span>protected</span> <span>void</span> <span>finalize</span>() <span>throws</span> Throwable { }Object类里定义了一个名为finalize的空方法,这意味着 Java 里的所有类都会继承这个方法,甚至可以覆写该方法,并且根据方法覆写原则,如果子类覆盖此方法,方法访问权限至少 protected 级别的,这样其子类就算没有覆写此方法也会继承此方法。

而判断当前类是否是 f 类的标准并不仅仅是当前类是否含有一个参数为空,返回值为 void 的finalize方法,还要求finalize 方法必须非空,因此 Object 类虽然含有一个finalize方法,但它并不是 f 类,Object 的对象在被 GC 回收时其实并不会调用它的finalize方法。

需要注意的是,类在加载过程中其实就已经被标记为是否为 f 类了。(JVM 在类加载的时候会遍历当前类的所有方法,包括父类的方法,只要有一个参数为空且返回 void 的非空finalize方法就认为这个类是 f 类。)

f 类的对象何时传到 Finalizer.register 方法

对象的创建其实是被拆分成多个步骤的,比如A a=new A(2)这样一条语句对应的字节码如下:

复制代码
<span>0</span>: <span>new #1 // class A</span>
<span>3</span>: <span>dup</span>
<span>4</span>: <span>iconst_2</span>
<span>5</span>: <span>invokespecial #11 // Method "<init>":(I)V</span>

先执行 new 分配好对象空间,然后再执行 invokespecial 调用构造函数,JVM 里其实可以让用户在这两个时机中选择一个,将当前对象传递给Finalizer.register方法来注册到Finalizer对象链里,这个选择取决于是否设置了RegisterFinalizersAtInit这个 vm 参数,默认值为 true,也就是在构造函数返回之前调用Finalizer.register方法,如果通过-XX:-RegisterFinalizersAtInit关闭了该参数,那将在对象空间分配好之后将这个对象注册进去。

另外需要提醒的是,当我们通过 clone 的方式复制一个对象时,如果当前类是一个 f 类,那么在 clone 完成时将调用Finalizer.register方法进行注册。

hotspot 如何实现 f 类对象在构造函数执行完毕后调用 Finalizer.register

这个实现比较有意思,在这简单提一下,我们知道执行一个构造函数时,会去调用父类的构造函数,主要是为了初始化继承自父类的属性,那么任何一个对象的初始化最终都会调用到Object的空构造函数里(任何空的构造函数其实并不空,会含有三条字节码指令,如下代码所示),为了不对所有类的构造函数都埋点调用Finalizer.register方法,hotspot 的实现是,在初始化Object类时将构造函数里的return指令替换为_return_register_finalizer指令,该指令并不是标准的字节码指令,是 hotspot 扩展的指令,这样在处理该指令时调用Finalizer.register方法,以很小的侵入性代价完美地解决了这个问题。

复制代码
<span>0</span>: <span>aload_0</span>
<span>1</span>: <span>invokespecial #21 // Method java/lang/Object."<init>":()V</span>
<span>4</span>: <span>return</span>

f 类对象的 GC 回收

FinalizerThread 线程

Finalizer类的clinit方法(静态块)里,我们看到它会创建一个FinalizerThread守护线程,这个线程的优先级并不是最高的,意味着在 CPU 很紧张的情况下其被调度的优先级可能会受到影响

复制代码
<span>private</span> <span>static</span> <span><span>class</span> <span>FinalizerThread</span> <span>extends</span> <span>Thread</span> {</span>
<span>private</span> <span>volatile</span> <span>boolean</span> running;
FinalizerThread(ThreadGroup g) {
<span>super</span>(g, <span>"Finalizer"</span>);
}
<span>public</span> <span>void</span> <span>run</span>() {
<span>if</span> (running)
<span>return</span>;
running = <span>true</span>;
<span>for</span> (;;) {
<span>try</span> {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer();
} <span>catch</span> (InterruptedException x) {
<span>continue</span>;
}
}
}
}
<span>static</span> {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
<span>for</span> (ThreadGroup tgn = tg;
tgn != <span>null</span>;
tg = tgn, tgn = tg.getParent());
Thread finalizer = <span>new</span> FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - <span>2</span>);
finalizer.setDaemon(<span>true</span>);
finalizer.start();
}

这个线程用来从 queue 里获取Finalizer对象,然后执行该对象的runFinalizer方法,该方法会将Finalizer对象从Finalizer对象链里剥离出来,这样意味着下次 GC 发生时就可以将其关联的 f 对象回收了,最后将这个Finalizer对象关联的 f 对象传给一个 native 方法invokeFinalizeMethod

复制代码
<span>private</span> <span>void</span> <span>runFinalizer</span>() {
<span>synchronized</span> (<span>this</span>) {
<span>if</span> (hasBeenFinalized()) <span>return</span>;
remove();
}
<span>try</span> {
Object finalizee = <span>this</span>.get();
<span>if</span> (finalizee != <span>null</span> && !(finalizee <span>instanceof</span> java.lang.Enum)) {
invokeFinalizeMethod(finalizee);
finalizee = <span>null</span>;
}
} <span>catch</span> (Throwable x) { }
<span>super</span>.clear();
}
<span>static</span> <span>native</span> <span>void</span> invokeFinalizeMethod(Object o) <span>throws</span> Throwable;

其实invokeFinalizeMethod方法就是调了这个 f 对象的 finalize 方法,看到这里大家应该恍然大悟了,整个过程都串起来了。

复制代码
JNIEXPORT <span>void</span> JNICALL
Java_java_lang_ref_Finalizer_invokeFinalizeMethod(JNIEnv *env, jclass clazz,
jobject ob)
{
jclass cls;
jmethodID mid;
<span><span>cls</span> = <span>(*env)</span>-></span>GetObjectClass(env, ob);
<span>if</span> (cls == NULL) <span>return</span>;
<span><span>mid</span> = <span>(*env)</span>-></span>GetMethodID(env, cls, <span>"finalize"</span>, <span>"()V"</span>);
<span>if</span> (mid == NULL) <span>return</span>;
<span><span>(*env)</span>-></span>CallVoidMethod(env, ob, mid);
}

f 对象的 finalize 方法抛出异常会导致 FinalizeThread 退出吗

不知道大家有没有想过如果 f 对象的finalize方法抛了一个没捕获的异常,这个FinalizerThread会不会退出呢,细心的读者看上面的代码其实就可以找到答案,runFinalizer方法里对Throwable的异常进行了捕获,因此不可能出现FinalizerThread因异常未捕获而退出的情况。

f 对象的 finalize 方法会执行多次吗

如果我们在 f 对象的finalize方法里重新将当前对象赋值,变成可达对象,当这个 f 对象再次变成不可达时还会执行finalize方法吗?答案是否定的,因为在执行完第一次finalize方法后,这个 f 对象已经和之前的Finalizer对象剥离了,也就是下次 GC 的时候不会再发现Finalizer对象指向该 f 对象了,自然也就不会调用这个 f 对象的finalize方法了。

Finalizer 对象何时被放到 ReferenceQueue 里

除了这里接下来要介绍的环节之外,整个过程大家应该都比较清楚了。

当 GC 发生时,GC 算法会判断 f 类对象是不是只被Finalizer类引用(f 类对象被Finalizer对象引用,然后放到Finalizer对象链里),如果这个类仅仅被Finalizer对象引用,说明这个对象在不久的将来会被回收,现在可以执行它的finalize方法了,于是会将这个Finalizer对象放到Finalizer类的ReferenceQueue里,但是这个 f 类对象其实并没有被回收,因为Finalizer这个类还对它们保持引用,在 GC 完成之前,JVM 会调用ReferenceQueue中 lock 对象的 notify 方法(当ReferenceQueue为空时,FinalizerThread线程会调用ReferenceQueue的 lock 对象的 wait 方法直到被 JVM 唤醒),此时就会执行上面 FinalizeThread 线程里看到的其他逻辑了。

Finalizer 导致的内存泄露

这里举一个简单的例子,我们使用挺广的 Socket 通信,SocksSocketImpl的父类其实就实现了finalize方法:

复制代码
<span>/**
* Cleans up if the user forgets to close it.
*/</span>
<span>protected</span> <span>void</span> <span>finalize</span>() <span>throws</span> IOException {
close();
}

其实这么做的主要目的是万一用户忘记关闭 Socket,那么在这个对象被回收时能主动关闭 Socket 来释放一些系统资源,但是如果用户真的忘记关闭,那这些socket对象可能因为FinalizeThread迟迟没有执行这些socket对象的finalize方法,而导致内存泄露,这种问题我们碰到过多次,因此对于这类情况除了大家好好注意貌似没有什么更好的方法了,该做的事真不能省.

Finalizer 的客观评价

上面的过程基本对Finalizer的实现细节进行了完整剖析,Java 里我们看到有构造函数,但是并没有看到析构函数一说,Finalizer其实是实现了析构函数的概念,我们在对象被回收前可以执行一些“收拾性”的逻辑,应该说是一个特殊场景的补充,但是这种概念的实现给 f 对象生命周期以及 GC 等带来了一些影响:

  • f 对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收;
  • f 对象至少经历两次 GC 才能被回收,因为只有在FinalizerThread执行完了 f 对象的finalize方法的情况下才有可能被下次 GC 回收,而有可能期间已经经历过多次 GC 了,但是一直还没执行 f 对象的finalize方法;
  • CPU 资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行 f 对象的finalize方法;
  • 因为 f 对象的finalize方法迟迟没有执行,有可能会导致大部分 f 对象进入到 old 分代,此时容易引发 old 分代的 GC,甚至 Full GC,GC 暂停时间明显变长;
  • f 对象的finalize方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。

作者简介

李嘉鹏花名寒泉子,使用「你假笨」的ID 混迹网络,蚂蚁金服高级研发工程师。本科毕业四年多,一直待在支付宝,先后从事过监控系统、框架容器以及性能分析系统等研发工作,其中从事框架容器三年多,主要负责开发支付宝的统一编程框架sofa,2014 年下半年开始重点从事性能分析系统的研发工作并于年底加入JVM 团队。


感谢丁晓昀对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015 年 7 月 09 日 00:328906

评论

发布
暂无评论
发现更多内容

架构师训练营 - 学习总结 - 第一周

桔子

食堂就餐卡系统设计

chinsun1

架构文档

架构师训练营-week1-学习总结

暖丶冬

第一周总结

王志祥

极客大学架构师训练营

【架构训练营】第一期

云064

Week 01 作业:食堂就餐卡系统设计

鱼_XueTr

架构师训练营第一周心得

努力努力再努力m

极客大学架构师训练营

架构师学习第一周作业

云峰

架构师0期 | 食堂就餐卡系统架构设计文档

刁架构

极客大学架构师训练营

练习1-1

闷骚程序员

Homework-食堂就餐卡系统设计

River Tree

架构设计 Homework

【总结】架构师要做什么

缺省模式

食堂就餐卡系统设计

食堂就餐系统架构设计(训练营第一课)

看山是山

极客大学架构师训练营 UML 食堂就餐系统

食堂就餐卡系统设计

史慧君

week1作业

数字

食堂就餐卡系统设计

泛岁月的涟漪

week1-食堂就餐卡系统

张健

食堂就餐卡

食堂就餐卡系统设计

拈香(曾德政)

架构设计 极客大学架构师训练营

sed命令基础

飞翔

Linux

食堂就餐卡系统设计

呱呱

极客大学架构师训练营 作业

架构师训练营第1周作业

无名氏

架构师-第一课总结

free[啤酒]

week1《作业二:根据当周学习情况,完成一篇学习总结》

任鑫

架构师训练营-学习笔记-第一周

superman

学习 极客大学架构师训练营

学习总结-架构师训练营-第一周

走过路过飞过

第二课 以图服人

Geek_bobo

食堂就餐卡系统设计

小遵

week1-学习总结

张健

week1.学习总结

个人练习生niki

架构师和架构

拈香(曾德政)

架构师 极客大学架构师训练营

2021年,算法还“香”吗?

2021年,算法还“香”吗?

JVM源码分析之FinalReference完全解读-InfoQ