天天看点

JVM源码分析之FinalReference完全解读

java对象引用体系除了强引用之外,出于对性能,可扩展性等方面考虑还特地实现了四种其他引用:<code>softreference</code>、<code>weakreference</code>、<code>phantomreference</code>、<code>finalreference</code>,本文主要想讲的是<code>finalreference</code>,因为zprofiler在分析一些oom的heap的时候,经常能看到 <code>java.lang.ref.finalizer</code>占用的内存大小远远排在前面(finalizer heap demo),而这个类占用的内存大小又和我们这次的主角<code>finalreference</code>及关联的内容可能给我们留下如下印象:

自己代码里从没有使用过;

线程dump之后,会看到一个叫做<code>finalizer</code>的java线程;

偶尔能注意到<code>java.lang.ref.finalizer</code>的存在;

在类里可能会写<code>finalize</code>方法。

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

首先我们看看<code>finalreference</code>在jdk里的实现:

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

<code>finalizer</code>的构造函数提供了以下几个关键信息:

private:意味着我们无法在当前类之外构建这类的对象;

finalizee参数:<code>finalreference</code>指向的对象引用;

调用add方法:将当前对象插入到<code>finalizer</code>对象链里,链里的对象和<code>finalizer</code>类静态关联。言外之意是在这个链里的对象都无法被gc掉,除非将这种引用关系剥离(因为<code>finalizer</code>类无法被unload)。

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

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

在讲这个问题之前,我们先来看下<code>java.lang.object</code>里的一个方法

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

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

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

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

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

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

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

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

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

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

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

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

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

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

这里举一个简单的例子,我们使用挺广的socket通信,<code>sockssocketimpl</code>的父类其实就实现了<code>finalize</code>方法:

其实这么做的主要目的是万一用户忘记关闭socket,那么在这个对象被回收时能主动关闭socket来释放一些系统资源,但是如果用户真的忘记关闭,那这些socket对象可能因为<code>finalizethread</code>迟迟没有执行这些socket对象的finalize方法,而导致内存泄露,这种问题我们碰到过多次,需要特别注意的是对于已经没有地方引用的这些f对象,并不会在最近的那一次gc里马上回收掉,而是会延迟到下一个或者下几个gc时才被回收,因为执行finalize方法的动作无法在gc过程中执行,万一finalize方法执行很长呢,所以只能在这个gc周期里将这个垃圾对象重新标活,直到执行完finalize方法从queue里删除,这样下次gc的时候就真的是漂浮垃圾了会被回收,因此给大家的一个建议是千万不要在运行期不断创建f对象,不然会很悲剧。

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

f对象因为<code>finalizer</code>的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收;

f对象至少经历两次gc才能被回收,因为只有在<code>finalizerthread</code>执行完了f对象的<code>finalize</code>方法的情况下才有可能被下次gc回收,而有可能期间已经经历过多次gc了,但是一直还没执行f对象的<code>finalize</code>方法;

cpu资源比较稀缺的情况下<code>finalizerthread</code>线程有可能因为优先级比较低而延迟执行f对象的<code>finalize</code>方法;

因为f对象的<code>finalize</code>方法迟迟没有执行,有可能会导致大部分f对象进入到old分代,此时容易引发old分代的gc,甚至full gc,gc暂停时间明显变长;

f对象的<code>finalize</code>方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。

该文章来自于阿里巴巴技术协会(ata)精选文章

个人公众号:

JVM源码分析之FinalReference完全解读