JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
发布网友
发布时间:22小时前
我来回答
共1个回答
热心网友
时间:21小时前
JAVA对象引用体系除了强引用之外,还特地实现了一系列其他引用以满足性能和可扩展性需求:SoftReference、WeakReference、PhantomReference、FinalReference。本文将重点探讨FinalReference,因其在内存分析工具如MAT中的高占用率引起了广泛关注。在JDK中,FinalReference的实现为package级别访问权限,实际使用中我们无法直接扩展。然而,为了满足特定需求,JDK通过扩展实现了一个名为java.lang.ref.Finalizer的类,同样具有package级别访问权限且为final,确保不可被扩展。接下来,我们将围绕Finalizer类展开讨论,关注其构造函数、对象注册过程以及内存管理机制。
首先,让我们关注Finalizer的构造函数。构造函数具有private访问权限,意味着我们无法自行创建此类实例。构造函数接收一个参数,即FinalReference指向的对象引用。在构造函数中,FinalReference对象被插入到Finalizer对象链中,这是一个静态关联,表明在该链中的对象无法被垃圾回收(GC)处理,除非通过某种方式解除引用关系,因为Finalizer类无法被卸载。
虽然我们无法直接创建Finalizer对象,但有一个名为register的静态方法,它在方法内部创建此类对象,并将其加入到Finalizer对象链。这个方法由虚拟机(VM)调用。那么,VM在什么情况下会调用这个方法呢?答案与类的修饰符有关,特别是final修饰符。当一个类被final修饰时,我们称之为f类。在处理f类对象时,GC会在对象被回收前调用其finalize方法。
接下来,我们探讨如何判断一个类是否为f类。在Java世界中,所有类继承了名为finalize的方法,即使没有覆写该方法,也会继承此方法。然而,判断一个类是否为f类的标准并不仅仅是包含一个参数为空、返回值为void的名为finalize的方法。另一个关键点是finalize方法必须非空。因此,虽然Object类包含一个finalize方法,但它本身并非f类,其对象在GC回收时不调用其finalize方法。
需要注意的是,f类对象的创建过程涉及多个步骤,例如在表达式如“new A(2)”中,对象首先被分配空间,然后调用构造函数。在这个过程中,我们可以通过设置VM参数-XX:RegisterFinalizersAtInit来选择在构造函数调用前或对象空间分配后将对象注册到Finalizer对象链中。默认情况下,此参数为true,意味着在构造函数返回前注册。若关闭此参数,注册将在对象空间分配后立即进行。
Hotspot实现中,通过在Object类初始化时替换构造函数的return指令为_return_register_finalizer指令,这是一个非标准字节码指令,用于在处理此指令时调用Finalizer.register方法。这一实现巧妙地解决了对所有类构造函数的侵入性问题。
在GC回收过程中,f类对象通过FinalizerThread线程处理。该线程从queue中取出Finalizer对象,执行runFinalizer方法,将Finalizer对象从链中剥离,并传递给native方法invokeFinalizeMethod,调用f对象的finalize方法。整个过程紧密相连,确保f类对象在合适时机执行清理逻辑。
Finalizer对象何时被放入ReferenceQueue?当GC发生时,它会判断f类对象是否仅由Finalizer类引用。若满足条件,表示即将被回收且可以执行finalize方法,Finalizer对象将被放入Reference.pending字段中。然而,f类对象并未真正被回收,因为Finalizer类仍保持对其引用。在GC完成后,jvm通过调用ReferenceHandler线程的wait方法唤醒,该线程处理Reference.pending为空时调用notify方法,最终将Finalizer对象放入Finalizer类的ReferenceQueue中,从而触发FinalizerThread执行后续逻辑。
关于f对象的finalize方法抛出异常,实际上不会导致FinalizerThread退出,因为在runFinalizer方法中对Throwable异常进行了捕获处理。
如果我们在f对象的finalize方法中重新将当前对象赋值为可达状态,当此f对象再次变为不可达时,它不会再执行finalize方法。因为在执行完第一次方法后,f对象与之前绑定的Finalizer对象关系被解除,下次GC时不会再发现该对象与Finalizer对象的关联,从而不会再次调用其finalize方法。
Finalizer可能导致内存泄漏的问题。以Socket通信为例,SocksSocketImpl的父类实现了finalize方法,其主要目的是在对象被回收前主动关闭socket以释放资源。然而,如果用户忘记关闭socket,导致内存泄露。多次遇到此类问题后,建议避免在运行期不断创建f对象,以防止悲剧发生。
总结而言,Finalizer实现了析构函数的概念,允许我们在对象被回收前执行清理逻辑。然而,这种机制对对象生命周期和GC过程产生了影响。了解Finalizer的实现细节有助于我们更好地管理内存使用,避免潜在的内存泄漏问题。