“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

JVM 源码分析之 FinalReference 完全解读

  • 2015-07-09
  • 本文字数:6341 字

    阅读完需:约 21 分钟

概述

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-07-09 00:3210241

评论

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

如何用 Excel 做数据分析,提升你的工作效率?

搞大屏的小北

提升效率 Excel 数据可视化 DataEase

如何选择数据可视化图表?

搞大屏的小北

亚马逊云科技 2022 re:Invent 观察 | 天下武功,唯快不破

亚马逊云科技 (Amazon Web Services)

亚马逊云科技 Builder 专栏

《天翼云安全白皮书》发布!共铸国云安全生态!

天翼云开发者社区

SiamFC:用于目标跟踪的全卷积孪生网络 fully-convolutional siamese networks for object tracking

代码的路

图像处理

SiamRPN:High Performance Visual Tracking with Siamese Region Proposal Network 孪生网络

代码的路

神经网络

Kubernetes HPA 的三个误区与避坑指南

阿里巴巴中间件

阿里云 Kubernetes 云原生

数据分析原来还可以这么搞?

搞大屏的小北

数据分析 知乎 数据分析工具

DataEase 在 Windows 系统下的 jar 包部署

搞大屏的小北

DataEase 本地源码启动

搞大屏的小北

自编码器 AE(AutoEncoder)程序

代码的路

自编码器

小令观点 | 数字世界里,拿什么来保护你的身份安全?

令牌云数字身份

身份安全 人脸识别 安全技术

SiamRPN++: Evolution of Siamese Visual Tracking with Very Deep Networks 深层网络连体视觉跟踪的演变

代码的路

神经网络

什么是云渲染?云渲染速度快吗?

Renderbus瑞云渲染农场

云渲染 云渲染是什么 云渲染速度快吗

国内外开源数据可视化工具对比:DataEase相较于MetaBase有何优势

搞大屏的小北

DataEase Metabase 数据可视化工具对比 对比

DataEase数据集定时同步任务报错解决

搞大屏的小北

异常 报错 DataEase 数据集定时同步任务

portraiture2024最新版磨皮插件下载安装教程

茶色酒

Portraiture2023 Portraiture

又一创新!阿里云 Serverless 调度论文被云计算顶会 ACM SoCC 收录

阿里巴巴中间件

阿里云 Serverless 云原生

DataEase 数据源插件开发——如何替换 STGroupFile 模板文件

搞大屏的小北

数据可视化工具 DataEase STGroupFile 模版替换 数据源插件

开源数据可视化/自服务BI工具哪家强?

搞大屏的小北

数据可视化工具 DataEase 行转列

DataEase 在 Mac 系统下的 jar 包部署

搞大屏的小北

DataEase Mac 系统 jar 包部署

效能指标「研发浓度」在项目度量中的应用

feijieppm

项目管理 技术管理 文化 & 方法 效能度量 #研发效能

【DBA100人】白鳝:一直往上走,从程序员到数据库专家

OceanBase 数据库

数据库 oceanbase

IoT设备接入物联网平台华北2(北京) 节点开发实战——实践类

阿里云AIoT

小程序 监控 物联网 消息中间件 弹性计算

中华财险进击数字化

OceanBase 数据库

数据库 oceanbase

SA-Siam:用于实时目标跟踪的孪生网络A Twofold Siamese Network for Real-Time Object Tracking

代码的路

神经网络

场景 | 九科信息大型制造企业RPA数字化解决方案

九科Ninetech

再谈持续测试

FunTester

转租、重组、裁员,Salesforce给中国学徒带来了哪些启示?

ToB行业头条

CLIPPO:纯图像的CLIP,参数减半且更强大!

Zilliz

机器学习

作业帮:探索多云架构下的数据库集群解决方案

OceanBase 数据库

数据库 oceanbase

JVM源码分析之FinalReference完全解读_Java_李嘉鹏_InfoQ精选文章