`

JVM源码分析之FinalReference完全解读

    博客分类:
  • jvm
jvm 
阅读更多

http://lovestblog.cn/blog/2015/07/09/final-reference/

概述

JAVA对象引用体系除了强引用之外,出于对性能、可扩展性等方面考虑还特地实现了四种其他引用: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里的实现:

 
classFinalReference<T>extendsReference<T>{publicFinalReference(T referent,ReferenceQueue<?super T> q){super(referent, q);}}

大家应该注意到了类访问权限是package的,这也就意味着我们不能直接去对其进行扩展,但是JDK里对此类进行了扩展实现java.lang.ref.Finalizer,这个类也是我们在概述里提到的,而此类的访问权限也是package的,并且是final的,意味着真的不能被扩展了,接下来的重点我们围绕java.lang.ref.Finalizer展开(PS:后续讲Finalizer相关的其实也就是在说FinalReference)

 
finalclassFinalizerextendsFinalReference{/* 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
     */staticnativevoid invokeFinalizeMethod(Object o)throwsThrowable;privatestaticReferenceQueue queue =newReferenceQueue();privatestaticFinalizer unfinalized =null;privatestaticfinalObjectlock=newObject();privateFinalizernext=null,        prev =null;privateFinalizer(Object finalizee){super(finalizee, queue);        add();}/* Invoked by VM */staticvoidregister(Object finalizee){newFinalizer(finalizee);}privatevoid add(){synchronized(lock){if(unfinalized !=null){this.next= unfinalized;                unfinalized.prev =this;}            unfinalized =this;}}...}

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里的一个方法

 
protectedvoid finalize()throwsThrowable{}

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

而判断当前类是否是一个f类的标准并不仅仅是当前类是否含有一个参数为空,返回值为void的名为finalize的方法,而另外一个要求是finalize方法必须非空,因此我们的Object类虽然含有一个finalize方法,但是并不是一个f类,Object的对象在被gc回收的时候其实并不会去调用它的finalize方法。

需要注意的是我们的类在被加载过程中其实就已经被标记为是否为f类了(遍历所有方法,包括父类的方法,只要有一个非空的参数为空返回void的finalize方法就认为是一个f类)。

f类的对象何时传到Finalizer.register方法

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

 
0:new#1                  // class A3: 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>":()V4:return

f类对象的GC回收

FinalizerThread线程

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

 
privatestaticclassFinalizerThreadextendsThread{privatevolatileboolean running;FinalizerThread(ThreadGroup g){super(g,"Finalizer");}publicvoid 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 =newFinalizerThread(tg);        finalizer.setPriority(Thread.MAX_PRIORITY -2);        finalizer.setDaemon(true);        finalizer.start();}

这个线程主要就是从queue里取Finalizer对象,然后执行该对象的runFinalizer方法,这个方法主要是将Finalizer对象从Finalizer对象链里剥离出来,这样意味着下次gc发生的时候就可能将其关联的f对象gc掉了,最后将这个Finalizer对象关联的f对象传给了一个native方法invokeFinalizeMethod

 
privatevoid 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();}staticnativevoid invokeFinalizeMethod(Object o)throwsThrowable;

其实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.
 */protectedvoid finalize()throwsIOException{      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,甚至fullgc,gc暂停时间明显变长 * f对象的finalize方法被调用了,但是这个对象其实还并没有被回收,虽然可能在不久的将来会被回收

分享到:
评论

相关推荐

    jdk,jvm源码

    个人网站可能提供了一些关于JVM源码分析的教程和资源,对于深入学习JVM的运行机制,这是一个很好的参考资料。通过结合理论知识与实际源码阅读,可以更好地掌握Java编程的精髓,提高解决复杂问题的能力。

    JVM源码分析之Attach机制实现完全解读

    Attach是什么 在讲这个之前,我们先来点大家都知道的东西,当我们感觉线程一直卡在某个地 方,想知道卡在哪里,首先想到的是进行线程dump,而常用的命令是jstack ,我们就可以看到 如下线程栈了 2014­06­18 12...

    jvm源码jvm源码jvm源码

    jvm源码

    openjdk8u60+jvm jdk源码+jvm源码

    JVM 的源码对于理解 Java 性能优化、垃圾回收机制、类加载过程以及内存管理等方面有着至关重要的作用。HotSpot 是 Oracle JDK 和 OpenJDK 使用的默认 JVM 实现,它的名字来源于其“热点代码”检测技术,能够识别并...

    JVM源码C++-openjdk8u.zip

    jdk源码,jvm源码

    hotspot(jvm 源码).zip

    java JWM 源码 ,版本jdk1.8 。java JVM 源码,版本 jdk 1.8。java JWM 源码 ,版本jdk1.8 。java JWM 源码 ,版本jdk1.8 。java JWM 源码 ,版本jdk1.8 。

    jvm源码解读以及jvm调优,看的过程中会把c-c++文件也会放到里面进行解读-jvm-original.zip

    本资料主要探讨JVM的源码解读以及如何进行JVM调优,同时也涉及到了JVM内部实现中C/C++部分的解析。 1. **JVM结构概述** JVM由类装载器、运行时数据区、执行引擎、本地方法接口和本地方法库五大部分组成。其中,类...

    (二)MATJVM 内存分析工具.MAT JVM 内存分析工具.MAT JVM 内存分析工具.

    MAT JVM内存分析工具可以帮助开发者深入理解Java虚拟机(JVM)的内存管理机制,通过分析堆内存快照来识别内存消耗异常的情况。 首先,MAT提供了丰富的视图来帮助用户查看内存状态,例如“概述”视图可以快速了解堆...

    mat(mac)---jvm内存分析工具

    MAT,全称Memory Analyzer Tool,是IBM开发的一款强大的Java虚拟机(JVM)内存分析工具,尤其适用于Mac OS X平台。这款工具可以帮助开发者诊断和解决Java应用中的内存泄漏问题,提高应用性能。MAT通过深入分析堆转储...

    Jvm堆栈dump文件分析

    5. **解读和优化**:根据分析结果调整JVM参数、优化代码、修复内存泄漏等问题,然后重新测试以验证改进效果。 总的来说,HeadAnalyzer 4.1.4是WebSphere环境下Java性能调优的重要工具,通过深入解析dump文件,它能...

    深入理解jvm源码

    《深入理解Java虚拟机》是Java开发者深入了解JVM(Java Virtual Machine)的必备书籍,尤其对于想要提升技术深度、优化程序性能的工程师来说,更是不可或缺的参考资料。这本书的第二版全面覆盖了JVM的最新发展,包括...

    探索JVM底层奥秘ClassLoader源码分析与案例讲解

    Java虚拟机(JVM)是Java程序运行的基础,它负责加载、验证、执行字节码。在JVM的运行机制中,ClassLoader起着至关重要的作用...通过源码分析和实际案例,我们可以更深入地掌握JVM的工作原理,提升我们的Java开发技能。

    jvm内存分析工具mat

    MAT,全称Memory Analyzer Tool,是IBM开发的一款强大的Java虚拟机(JVM)内存分析工具。它主要用于诊断Java应用程序的内存泄漏问题,帮助开发者理解内存消耗情况,优化内存配置,从而提升应用性能。MAT以其易用性和...

    jvm内存分析工具mat安装包

    MAT,全称Memory Analyzer Tool,是IBM开发的一款强大的JVM内存分析工具,尤其适用于诊断Java应用程序的内存泄漏问题。在Java开发过程中,内存溢出(Out Of Memory)问题常常会导致程序异常终止,而MAT就是解决这类...

    javajvm源码学习-OpenJDK-Study:jvm源码学习

    java jvm 源码学习 学习 JVM 源码 并添加注解

    jvm 内存分析文档

    【Jvm 内存分析文档】 Java 虚拟机(JVM)是Java程序的核心运行环境,它负责管理和执行字节码。JVM内存管理主要包括内存结构、内存分配以及垃圾回收(GC)等方面。了解这些知识对于优化Java应用程序的性能至关重要...

    jvm调优.zip & hotspot源码解读

    《JVM调优与Hotspot源码解读》 在Java编程世界中,JVM(Java Virtual Machine)扮演着至关重要的角色。它负责运行Java应用程序,是Java“一次编写,到处运行”理念的核心。JVM调优是提升Java应用性能的关键步骤,而...

    java之jvm学习笔记十一(访问控制器)-源码

    本文将深入探讨JVM中的访问控制器,主要基于“java之jvm学习笔记十一(访问控制器)-源码”这一主题,以及相关的源码分析。 首先,我们得了解Java的安全模型。Java安全模型基于一种称为安全管理器(SecurityManager)...

    JVM分析工具

    为了确保应用的高效运行和优化,开发者通常会使用一系列的JVM分析工具。以下是对这些工具的详细介绍: 1. **jmap**: `jmap` 是一个命令行工具,它允许开发者获取堆内存的详细信息,包括堆的配置、对象统计、类...

    JVM 内存分析

    标签 "源码" 可能意味着博主深入探讨了JVM内存管理的底层实现,可能涉及了HotSpot JVM的部分源码解析,帮助读者理解内存分配、垃圾回收的具体步骤。 "工具" 标签表明博主可能分享了一些用于分析JVM内存的实用工具,...

Global site tag (gtag.js) - Google Analytics