当前位置: 首页 > 科技观察

JVM源码分析FinalReference完整解读

时间:2023-03-17 18:36:02 科技观察

概述JAVA对象引用系统除了强引用之外,出于性能和扩展性的考虑,还实现了另外四种引用:SoftReference、WeakReference、PhantomReference、FinalReference,这篇文章主要是我要的要说的就是FinalReference,因为我们在使用zprofiler、mat等内存分析工具分析一些oom堆时,经常可以看到java.lang.ref.Finalizer占用的内存大小遥遥领先,而这个类占用的内存大小离不开我们的主角FinalReference。对于FinalReference和相关内容,我们可能会有这样的印象:在我们的代码中从未使用过threaddump之后,我们可以看到一个名为Finalizer的java线程,偶尔会注意到java.lang.ref.Finalizer的存在。我们在类中可能会写finalize方法在里面,那么FinalReference存在的意义是什么,它以什么形式与我们的代码相关,这是本文要搞清楚的问题。JDK中的FinalReference首先我们看一下FinalReference在JDK中的实现:大家应该注意到这个类的访问权限是package,也就是说我们不能直接对其进行扩展,但是这个类在JDK中是有实现的。java.lang.ref.Finalizer,在概述中也提到了这个类,而且这个类的访问权限也是package和final,也就是说真的不能扩展。接下来的重点是java.lang.ref.Finalizer扩展(PS:后面讲的Finalizer其实就是FinalReference)Finalizer的构造函数我们从构造函数中得到以下关键信息*private:表示我们不能通过自己外部类对象*finalizee参数:FinalReference指向的对象引用*调用add方法:将当前对象插入到Finalizer对象链中,链中的对象与Finalizer类静态关联。这意味着这个链中的对象不能被gc删除,除非引用关系被剥离(因为无法卸载Finalizer类)。虽然外面不能创建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中的一个方法。在Object类中,定义了一个名为finalize的空方法,这意味着所有的类都会继承这个方法,甚至可以重写这个方法,根据方法重写的原则,如果子类重写了这个方法,方法的访问权限是至少是protected级别的,这样即使它的子类没有重载这个方法也会继承这个方法。判断当前类是否为f类的标准不仅是当前类是否包含参数为空返回值为void的名为finalize的方法,还有一个要求是finalize方法必须非空,所以我们的Object类虽然包含finalize方法,但它不是f类。Object的对象在被gc回收的时候,是不会调用它的finalize方法的。需要注意的是我们的类在加载过程中其实已经被标记为是否为f类(遍历所有方法,包括父类方法,只要有非null参数为空返回voidfinalize方法被认为是类别f)。当类f的对象传递给Finalizer.register方法时。对象的创建实际上分为多个步骤。比如Aa=newA(2)这样的语句对应的字节码是这样的:先执行new并分配Object空间,然后执行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_register_finalizer指令替换构造函数中的return指令。这条指令不是标准的字节码指令,而是hotspot扩展的一条指令,这样在处理指令的时候会调用Finalizer.register方法,完全解决了这个问题,侵入性很小。f类对象的GC回收了FinalizerThread线程。在Finalizer类的clinit方法(静态块)中,我们可以看到它会创建一个FinalizerThread守护线程。这个线程的优先级不是最高的,也就是说CPU很吃紧。在这种情况下,它的预定优先级可能会受到影响。该线程主要是从队列中取出Finalizer对象,然后执行该对象的runFinalizer方法。该方法主要是将Finalizer对象从Finalizer对象链中分离出来,也就是说下一次gc发生时,与其关联的f对象可能会被gcdropped。***将与Finalizer对象关联的f对象传递给本地方法invokeFinalizeMethod。实际上,invokeFinalizeMethod方法调用了f对象的finalize方法。看到这里,大家应该恍然大悟了,整个过程是连在一起的。如果f对象的finalize方法抛出异常,会不会导致FinalizeThread退出?它不会退出。细心的读者其实看上面的代码就能找到答案。Throwable异常是在runFinalizer方法中捕获的,所以FinalizerThread不可能因为未捕获的异常而退出。f对象的finalize方法会被执行多次吗?如果我们在f对象的finalize方法中重新赋值current对象,成为可达对象,那么当f对象再次变得不可达时,是否会执行finalize方法呢??答案是否定的,因为在执行完第一个finalize方法之后,f对象已经和之前的Finalizer对象分离了,也就是下一次gc的时候不会再发现Finalizer对象指向f对象。自然不会调用这个f对象的finalize方法。当Finalizer对象被放入ReferenceQueue时,除了这里要说的环节外,整个过程大家应该都清楚了。当gc发生时,gc算法会判断f类对象是否只被Finalizer类引用(f类对象被Finalizer对象引用,然后放到Finalizer对象链中),如果这个类只被Finalizer类引用Finalizer对象,表示这个对象近期会被回收,现在可以执行它的finalize方法,所以Finalizer对象会被放到Finalizer类的ReferenceQueue中,但是f类对象实际上并没有被回收,因为Finalizer类仍然有效它们持有引用。在gc完成之前,jvm会调用ReferenceQueue中锁对象的notify方法(当ReferenceQueue为空时,FinalizerThread线程会调用ReferenceQueue中锁对象的wait方法,直到被jvm唤醒).这时候,执行上面FinalizeThread线程中看到的其他逻辑。Finalizer引起的内存泄漏这里举一个简单的例子。我们广泛使用套接字通信。SocksSocketImpl的父类实际上实现了finalize方法:其实这样做的主要目的是万一用户忘记关闭socket,那么在这个对象被回收的时候,可以主动关闭socket来释放一些系统资源,但是如果用户真的忘记关闭它,那么这些socket对象可能会导致内存泄漏,因为FinalizeThread已经很久没有执行这些socket对象的finalize方法了。我们已经多次遇到这个问题。需要特别注意的是,这些没有地方引用的f对象,在最近一次gc中不会立即被回收,而是会延迟到下一次或者接下来的几次gc中被回收,因为执行finalize方法的动作无法执行在gc过程中。万一finalize方法执行的时间比较长,垃圾对象只能在本次gc循环中被remark,直到finalize方法执行完,从队列Delete中移除,这样下次gc真正漂浮的垃圾才会被回收,所以给大家的一个建议就是不要在运行时不断的创建f对象,否则会很悲剧。Finalizer的客观评价以上过程基本完整的分析了Finalizer的实现细节。在java中,我们看到有构造函数,却没有看到析构函数。终结器实际上实现了析构函数的概念。我们可以在对象被回收前执行一些“清零”逻辑,应该说是针对特殊场景的补充,但是这个概念的实现对我们的f对象生命周期和gc:f对象带来了一些影响,因为Finalizer的引用成为一个临时的强引用。即使没有其他强引用,f对象也不能在至少两次GC后立即被回收,因为只有当FinalizerThread执行完f对象的finalize方法后才有可能被下一次gc回收,而它是可能是期间经历了多次gc,但是f对象的finalize方法还没有执行。在cpu资源比较匮乏的情况下,FinalizerThread线程可能会因为优先级低而被延迟。执行f对象的finalize方法。因为f对象的finalize方法长时间没有执行,可能会导致大部分f对象进入老年代。这时候很容易造成老年代的gc,甚至是fullgc,而且gc停顿时间明显变长f对象的finalize方法已经调用了,但是这个对象还没有被回收,虽然近期可能会被回收【本文为专栏作家李嘉鹏原创文章,转载请通过微信公众号(你是假的,id:lovestblog)联系作者本人获取授权】点此阅读作者的更多好文章