JVM源码分析之FinalReference完全解读
Java对象引用体系除了强引用之外,出于对性能、可扩展性等方面考虑还特地实现了4种其他引用:SoftReference
、WeakReference
、PhantomReference
、FinalReference
,本文主要想讲的是FinalReference
,因为当使用内存分析工具,比如zprofiler、mat等,分析一些oom的heap时,经常能看到 java.lang.ref.Finalizer
占用的内存大小远远排在前面,而这个类占用的内存大小又和我们这次的主角FinalReference
有着密不可分的关系。
FinalReference
及关联的内容可能给我们留下如下印象:
- 自己代码里从没有使用过;
- 线程dump之后,会看到一个叫做
Finalizer
的Java线程; - 偶尔能注意到
java.lang.ref.Finalizer
的存在; - 在类里可能会写
finalize
方法。
那FinalReference
到底存在的意义是什么,以怎样的形式和我们的代码相关联呢?这是本文要理清的问题。
JDK中的FinalReference
首先我们看看FinalReference
在JDK里的实现:
class FinalReference<T> extends Reference<T> {
public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
大家应该注意到了类访问权限是package的,这也就意味着我们不能直接去对其进行扩展,但是JDK里对此类进行了扩展实现java.lang.ref.Finalizer
,这个类在概述里提到的过,而此类的访问权限也是package的,并且是final的,意味着它不能再被扩展了,接下来的重点我们围绕java.lang.ref.Finalizer
展开。(PS:后续讲的Finalizer
其实也是在说FinalReference
。)
final class Finalizer extends FinalReference { /* Package-private; must be in
same package as the Reference
class */
/* A native method that invokes an arbitrary object's finalize method is
required since the finalize method is protected
*/
static native void invokeFinalizeMethod(Object o) throws Throwable;
private static ReferenceQueue queue = new ReferenceQueue();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();
private Finalizer
next = null,
prev = null;
private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
...
}
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
里的一个方法
protected void finalize() throws 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)
这样一条语句对应的字节码如下:
0: new #1 // class A
3: dup
4: iconst_2
5: invokespecial #11 // Method "<init>":(I)V
先执行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
方法,以很小的侵入性代价完美地解决了这个问题。
0: aload_0
1: invokespecial #21 // Method java/lang/Object."<init>":()V
4: return
f类对象的GC回收
FinalizerThread线程
在Finalizer
类的clinit
方法(静态块)里,我们看到它会创建一个FinalizerThread
守护线程,这个线程的优先级并不是最高的,意味着在CPU很紧张的情况下其被调度的优先级可能会受到影响
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
if (running)
return;
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer();
} catch (InterruptedException x) {
continue;
}
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
这个线程用来从queue里获取Finalizer
对象,然后执行该对象的runFinalizer
方法,该方法会将Finalizer
对象从Finalizer
对象链里剥离出来,这样意味着下次GC发生时就可以将其关联的f对象回收了,最后将这个Finalizer
对象关联的f对象传给一个native方法invokeFinalizeMethod
private void runFinalizer() {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
invokeFinalizeMethod(finalizee);
/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
static native void invokeFinalizeMethod(Object o) throws Throwable;
其实invokeFinalizeMethod
方法就是调了这个f对象的finalize方法,看到这里大家应该恍然大悟了,整个过程都串起来了。
JNIEXPORT void JNICALL
Java_java_lang_ref_Finalizer_invokeFinalizeMethod(JNIEnv *env, jclass clazz,
jobject ob)
{
jclass cls;
jmethodID mid;
cls = (*env)->GetObjectClass(env, ob);
if (cls == NULL) return;
mid = (*env)->GetMethodID(env, cls, "finalize", "()V");
if (mid == NULL) return;
(*env)->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
方法:
/**
* Cleans up if the user forgets to close it.
*/
protected void finalize() throws 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
方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。
JVM源码分析之FinalReference完全解读的更多相关文章
- JVM源码分析之SystemGC完全解读
JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...
- JVM源码分析之栈溢出完全解读
概述 之所以想写这篇文章,其实是因为最近有不少系统出现了栈溢出导致进程crash的问题,并且很隐蔽,根本原因还得借助coredump才能分析出来,于是想从JVM实现的角度来全面分析下栈溢出的这类问题, ...
- JVM源码分析之堆外内存完全解读
JVM源码分析之堆外内存完全解读 寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...
- JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...
- JVM源码分析之一个Java进程究竟能创建多少线程
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
- JVM源码分析之Metaspace解密
概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...
- JVM源码分析-JVM源码编译与调试
要分析JVM的源码,结合资料直接阅读是一种方式,但是遇到一些想不通的场景,必须要结合调试,查看执行路径以及参数具体的值,才能搞得明白.所以我们先来把JVM的源码进行编译,并能够使用GDB进行调试. 编 ...
- JVM源码分析-类加载场景实例分析
A类调用B类的静态方法,除了加载B类,但是B类的一个未被调用的方法间接使用到的C类却也被加载了,这个有意思的场景来自一个提问:方法中使用的类型为何在未调用时尝试加载?. 场景如下: public cl ...
- JVM源码分析之堆内存的初始化
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十五篇. 今天呢!灯塔君跟大家讲: JVM源码分析之堆内存的初始化 堆初始化 Java堆的初始化入口位于Univ ...
随机推荐
- 也来写写基于单表的Orm(使用Dapper)
前言 这两天看园子里有个朋友写Dapper的拓展,想到自己之前也尝试用过,但不顺手,曾写过几个方法来完成自动的Insert操作.而对于Update.Delete.Select等,我一直对Diction ...
- Duplicate the UIButton and Move it
http://stackoverflow.com/questions/19241208/duplicate-the-uibutton-and-move-it/26438692#26438692 1do ...
- 【原创】Linux环境下的图形系统和AMD R600显卡编程(10)——R600显卡的3D引擎编程
3D图形处理流水线需要流经多个硬件单元才能得到最后的渲染结果,流水线上的所有的硬件单元必须被正确编程,才能得到正确的结果. 总体上看,从图形处理流水线的源头开始,需要准备好vertex和index,在 ...
- python--easygui
1.msgbox import easygui as eg # msgbox # 一般使用三个参数,msg:内容,title:标题,ok_button:按钮内容 eg.msgbox(msg=" ...
- poj 1981(单位圆覆盖最多点问题模板)
Circle and Points Time Limit: 5000MS Memory Limit: 30000K Total Submissions: 7327 Accepted: 2651 ...
- 解决:laravel出现Please provide a valid cache path.
SVN检出版本库上的项目后,访问首页出现如下提示: Please provide a valid cache path. 解决方法如下: 1.确保storage目录下有如app,framework,v ...
- maven坐标
maven坐标 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat ...
- (1) SpringBoot创建发布
一.安装jdk8 https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 二.配置环境 ...
- poj3254(状态压缩DP)
poj3254 题意 给出一个01矩阵,1表示当前这个位置可以放牛,要求放牛的方案保证牛不能左右或上下相邻,求方案数. 分析 dp[S][i]: 表示到 i 行时的状态S(用二进制数表示),那么状态转 ...
- Spfa【p3385】【模板】负环(spfa)
顾z 你没有发现两个字里的blog都不一样嘛 qwq 题目描述 毒瘤数据要求判负环 分析: 还是融合了不少题解的思想的. 负环定义: 权值和为负的环 //在网络上并没有找到一个官方定义,暂且这么理解. ...