首页>>后端>>java->Java JVM的引用计数和可达性分析垃圾收集算法

Java JVM的引用计数和可达性分析垃圾收集算法

时间:2023-12-01 本站 点击:0

1 垃圾收集概述

在C/C++语言中,没有自动垃圾回收机制,是通过new关键字申请内存资源,通过delete关键字释放内存资源。如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源,最终可能会导致内存溢出。

为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC。有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。

不可能再被任何途径使用的对象,便可称之为垃圾,就可以被回收了。常见的垃圾分析算法有两种,一种是引用计数法,另一种是可达性分析算法。

2 引用计数算法

引用计数是最简单直接的一种方式,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,那么此对象就可以作为垃圾收集器的目标对象来收集。

优点:

简单,直接,不需要暂停整个应用。

缺点:

需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作;

不能处理循环引用的问题。比如对象 A 中有一个字段指向了对象 B ,而对象 B 中也有一个字段指向了对象 A,而事实上他们俩都不再使用,但计数器的值永远都不可能为 0 ,也就不会被回收,然后就发生了内存泄露。

如下案例:

publicclassReferenceCountingGC{privateObjectinstance;privatestaticvoidtestGC(){ReferenceCountingGCobjA=newReferenceCountingGC();ReferenceCountingGCobjB=newReferenceCountingGC();//objA中有objB,objB中有objAobjA.instance=objB;objB.instance=objA;//虽然objA和objB置空,但这是指将他们的引用置空,在堆内存中,这两个对象还是互相持有\依赖的,这就是循环引用。objA=null;objB=null;}publicstaticvoidmain(String[]args){testGC();}}

3 可达性分析算法

现在,在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,即可被回收,又称为GC Roots Tracing算法。如下图所示,对象object5、object6、object7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

3.1 可以作为GC Roots对象种类

在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区中类静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI(即一般说的Native方法)引用的对象。

3.2 两次标记

为了增加垃圾收集的灵活性。实际上,一个到GC Roots没有任何引用链相连的对象有可能在某一个条件下“ 复活” 自己。对象的状态可以简单分成三类:

可达的: 从根节点开始, 可以到达这个对象。(实际上可触及对象也分为四种:)

可复活的: 对象的所有引用都被释放, 但是对象有可能在finalize()函数中复活。

不可触及的:对象对象没有覆盖finalize()方法或者finalize()函数已经被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为finalize()函数只会被调用一次。

即使在一次可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法:当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

如果这个对象被判定为有必要执行finalize()方法,那么该对象将会被回收。

关于两次标记的Java代码源码,可以看这个文章:Java中的Finalizer类以及GC二次标记过程中的Java源码解析。

案例演示:

/**1.此代码演示了两点:2.1.对象可以在被GC时自我拯救。3.2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次4.5.@authorzzm*/publicclassFinalizeEscapeGC{privatestaticFinalizeEscapeGCSAVE_HOOK=null;privatestaticvoidisAlive(){if(SAVE_HOOK!=null){System.out.println("yes,iamstillalive:)");}else{System.out.println("no,iamdead:(");}}@Overrideprotectedvoidfinalize()throwsThrowable{super.finalize();System.out.println("finalizemehtodexecuted!");//持有引用,对象"复活"FinalizeEscapeGC.SAVE_HOOK=this;}publicstaticvoidmain(String[]args)throwsThrowable{SAVE_HOOK=newFinalizeEscapeGC();/*测试对象第一次成功拯救自己*///首先引用置null,该对象没有其他引用.SAVE_HOOK=null;//然后尝试进行一次gc,此时会执行回收SAVE_HOOK类,但是会执行finalize方法,在finalize中对其进行复活System.gc();//因为finalize方法由低优先级的线程finalizer调用,优先级很低,所以暂停0.5秒以等待它执行Thread.sleep(500);//测试是否复活isAlive();/*下面这段代码与上面的完全相同,但是这次自救却失败了。*/SAVE_HOOK=null;//然后再次尝试进行一次gc,此时会执行回收SAVE_HOOK类,但是不会执行finalize方法,因为fiinalize已经执行过一次了,第二次不会执行,这次自救却失败了System.gc();//因为finalize方法由低优先级的线程finalizer调用,优先级很低,所以暂停0.5秒以等待它Thread.sleep(500);isAlive();}}

代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

Java中只有构造函数并没有析构函数一说,这里finalize()方法看起来像是实现了析构函数,但这只是Java刚诞生时为了使C/C++程序员更容易接受它所做出的一个妥协。

由于调用的线程优先级很低,因此调用时间是不确定的,我们也无法主动调用。

改变了要被回收的对象的引用和生命周期,由于加入到队列中导致应该被回收的对象迟迟不被回收,造成内存泄漏。

某些流比如 InputStreamReader在关闭时,并不会关闭它的嵌套流,此时只能由finalize释放,但是如果开启的流很多,那么由于finalizer是单线程,可能造成释放速度小于加入队列速度,这时就会有大量的Finalizer堆积, 导致内存的异常。

现在,finalize()能做的所有工作,例如关闭外部资源等,使用try-finally或者其他方式都可以做得更好、更及时,因此finalize()方法不建议被使用。

析构函数(destructor): 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。

4 方法区/永久代的垃圾分析

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。

但是判断一个类是否是“无用的类”却需要同时满足下面3个条件才能行:

该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

加载该类的ClassLoader已经被回收。

该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。

在大量使用反射、动态代理, CGLib等Byt式ode框架、动态生成JSP 以及OSG,这类频繁(自定义ClassLoader的场景都需耍虔拟机具备类卸载的功能,以保证永久代不会溢出。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/java/5506.html