JAVA对象引用体系除了强引用以外,出于对性能、可扩展性等方面考虑还特意实现了四种其余引用:SoftReference、WeakReference、PhantomReference、FinalReference,本文主要想讲的是FinalReference,由于咱们在使用内存分析工具好比mat等在分析一些oom的heap的时候,常常能看到 java.lang.ref.Finalizer占用的内存大小远远排在前面(其实经过jmap -histo就能发现,以下图所示),而这个类占用的内存大小又和咱们此次的主角FinalReference有着密不可分的关系。java
对于FinalReference及关联的内容,咱们可能有以下印象:算法
那FinalReference到底存在的意义是什么,以怎样的形式和咱们的代码相关联呢,这是本文要理清的问题。segmentfault
首先咱们看看FinalReference在JDK里的实现:jvm
你们应该注意到了类访问权限是package的,这也就意味着咱们不能直接去对其进行扩展,可是JDK里对此类进行了扩展实现java.lang.ref.Finalizer,这个类也是咱们在概述里提到的,而此类的访问权限也是package的,而且是final的,意味着真的不能被扩展了,接下来的重点咱们围绕java.lang.ref.Finalizer展开(PS:后续讲Finalizer相关的其实也就是在说FinalReference)socket
从构造函数上咱们得到下面的几个关键信息函数
private:意味着咱们在外面没法本身构建这类对象工具
finalizee参数:FinalReference指向的对象引用性能
调用add方法:将当前对象插入到Finalizer对象链里,链里的对象和Finalizer类静态相关联,言外之意是在这个链里的对象都没法被gc掉,除非将这种引用关系剥离掉(由于Finalizer类没法被unload)。spa
虽然外面没法建立Finalizer对象,可是注意到有一个register的静态方法,在方法里会建立这种对象,同时将这个对象加入到Finalizer对象链里,这个方法是被vm调用的,那么问题来了,vm在什么状况下会调用这个方法呢?线程
类其实有挺多的修饰,好比final,abstract,public等等,若是一个类有final修饰,咱们就说这个类是一个final类,上面列的都是语法层面咱们能够显示标记的,在jvm里其实还给类标记其余一些符号,好比finalizer,表示这个类是一个finalizer类(为了和java.lang.ref.Fianlizer类进行区分,下文要提到的finalizer类的地方都说成f类),gc在处理这种类的对象的时候要作一些特殊的处理,如在这个对象被回收以前会调用一下它的finalize方法。
在讲这个问题以前,咱们先来看下java.lang.Object里的一个方法
在Object类里定义了一个名为finalize的空方法,这意味着Java世界里的全部类都会继承这个方法,甚至能够覆写该方法,而且根据方法覆写原则,若是子类覆盖此方法,方法访问权限都是至少是protected级别的,这样其子类就算没有覆写此方法也会继承此方法。
而判断当前类是不是一个f类的标准并不只仅是当前类是否含有一个参数为空,返回值为void的名为finalize的方法,而另一个要求是finalize方法必须非空,所以咱们的Object类虽然含有一个finalize方法,可是并非一个f类,Object的对象在被gc回收的时候其实并不会去调用它的finalize方法。
须要注意的是咱们的类在被加载过程当中其实就已经被标记为是否为f类了(遍历全部方法,包括父类的方法,只要有一个非空的参数为空返回void的finalize方法就认为是一个f类)。
对象的建立实际上是被拆分红多个步骤的,好比A a=new A(2)这样一条语句对应的字节码以下:
先执行new分配好对象空间,而后再执行invokespecial调用构造函数,jvm里其实可让用户选择在这两个时机中的任意一个将当前对象传递给Finalizer.register方法来注册到Finalizer对象链里,这个选择依赖于RegisterFinalizersAtInit这个vm参数是否被设置,默认值为true,也就是在调用构造函数返回以前调用Finalizer.register方法,若是经过-XX:-RegisterFinalizersAtInit关闭了该参数,那将在对象空间分配好以后就将这个对象注册进去。
另外须要提一点的是当咱们经过clone的方式复制一个对象的时候,若是当前类是一个f类,那么在clone完成的时候将调用Finalizer.register方法进行注册。
这个实现比较有意思,在这里简单提一下,咱们知道一个构造函数执行的时候,会去调用父类的构造函数,主要是为了能对继承自父类的属性也能作初始化,那么任何一个对象的初始化最终都会调用到Object的空构造函数里(任何空的构造函数其实并不空,会含有三条字节码指令,以下代码所示),为了避免对全部的类的构造函数都作埋点调用Finalizer.register方法,hotspot的实现是在Object这个类在作初始化的时候将构造函数里的return指令替换为_return_register_finalizer指令,该指令并非标准的字节码指令,是hotspot扩展的指令,这样在处理该指令的时候调用Finalizer.register方法,这样就在侵入性很小的状况下完美地解决了这个问题。
在Finalizer类的clinit方法(静态块)里咱们看到它会建立了一个FinalizerThread的守护线程,这个线程的优先级并非最高的,意味着在cpu很紧张的状况下其被调度的优先级可能会受到影响
这个线程主要就是从queue里取Finalizer对象,而后执行该对象的runFinalizer方法,这个方法主要是将Finalizer对象从Finalizer对象链里剥离出来,这样意味着下次gc发生的时候就可能将其关联的f对象gc掉了,最后将这个Finalizer对象关联的f对象传给了一个native方法invokeFinalizeMethod
其实invokeFinalizeMethod方法就是调了这个f对象的finalize方法,看到这里你们应该恍然大悟了,整个过程都串起来了
那究竟何时会将Finalizer对象丢到ReferenceQueue里呢?当gc发生的时候,gc算法会判断f类对象是否是只被Finalizer类引用(f类对象被Finalizer对象引用,而后放到Finalizer对象链里),若是这个对象仅仅被Finalizer对象引用的时候,说明这个对象在不久的未来会被回收了,能够执行它的finalize方法了,因而会将这个Finalizer对象放到Reference.pending字段里(是一个Reference对象,可是是链式的)。可是这个f类对象其实并无被回收,由于Finalizer这个类还对他们持有引用,在gc完成以前,jvm会调用java.lang.ref.Reference里的lock对象的notify方法(当Reference.pending为空的时候,有个专门处理引用的叫作ReferenceHandler的线程会一直在wait),ReferenceHandler这个线程在处理的时候会将对应的Finalizer对象丢到Finalizer类的ReferenceQueue里,此时由于ReferenceQueue非空了,因而FinalizerThread会执行上面FinalizeThread线程里看到的其余逻辑了。这个过程可能有点绕,最好是结合代码看看,下面简单绘了一个图
不知道你们有没有想过若是f对象的finalize方法抛了一个没捕获的异常,这个FinalizerThread会不会退出呢,细心的读者看上面的代码其实就能够找到答案,在runFinalizer方法里对Throwable的异常都进行了捕获,所以不可能出现FinalizerThread因异常未捕获而退出的状况。
若是咱们在f对象的finalize方法里从新将当前对象赋值出去,变成可达对象,当这个f对象再次变成不可达的时候还会被执行finalize方法吗?答案是否认的,由于在执行完第一次finalize方法以后,这个f对象已经和以前的Finalizer对象关系剥离了,也就是下次gc的时候不会再发现Finalizer对象指向该f对象了,天然也就不会调用这个f对象的finalize方法了。
这里举一个简单的例子,咱们使用挺广的socket通讯,SocksSocketImpl的父类其实就实现了finalize方法:
其实这么作的主要目的是万一用户忘记关闭socket了,那么在这个对象被回收的时候能主动关闭socket来释放一些系统资源,可是若是真的是用户忘记关闭了,那这些socket对象可能由于FinalizeThread迟迟没有执行到这些socket对象的finalize方法,而致使内存泄露,这种问题咱们碰到过屡次,须要特别注意的是对于已经没有地方引用的这些f对象,并不会在最近的那一次gc里立刻回收掉,而是会延迟到下一个或者下几个gc时才被回收,由于执行finalize方法的动做没法在gc过程当中执行,万一finalize方法执行很长呢,因此只能在这个gc周期里将这个垃圾对象从新标活,直到执行完finalize方法从queue里删除,这样下次gc的时候就真的是漂浮垃圾了会被回收,所以给你们的一个建议是千万不要在运行期不断建立f对象,否则会很悲剧。
上面的过程基本对Finalizer的实现细节进行完整剖析了,java里咱们看到有构造函数,可是并无看到析构函数一说,Finalizer实际上是实现了析构函数的概念,咱们在对象被回收前能够执行一些『收拾性』的逻辑,应该说是一个特殊场景的补充,可是这种概念的实现给咱们的f对象生命周期以及gc等带来了一些影响:
推荐阅读 ZGC何时会进行垃圾回收
推荐阅读 GC一些长时间停顿问题排查及解决办法