JVM-垃圾回收
jvm大局观之内存管理篇(三):java如何判断哪些对象该被回收
1. JVM中的垃圾回收器-总览
针对新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New。这三个采用的都是标记 - 复制算法。其中,Serial 是一个单线程的,Parallel New 可以看成 Serial 的多线程版本。Parallel Scavenge 和 Parallel New 类似,但更加注重吞吐率。此外,Parallel Scavenge 不能与 CMS 一起使用。
总而言之,当发生 Minor GC 时,我们应用了标记 - 复制算法,将 Survivor 区中的老存活对象晋升到老年代,然后将剩下的存活对象和 Eden 区的存活对象复制到另一个 Survivor 区中。理想情况下,Eden 区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记 - 复制算法的效果极好。
Minor GC 的另外一个好处是不用对整个堆进行垃圾回收。但是,它却有一个问题,那就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。
这样一来,岂不是又做了一次全堆扫描呢?-- 卡表-这个技术是用于解决减少老年代的全堆空间扫描
针对老年代的垃圾回收器也有三个:刚刚提到的 Serial Old 和 Parallel Old,以及 CMS。Serial Old 和 Parallel Old 都是标记 - 压缩算法。同样,前者是单线程的,而后者可以看成前者的多线程版本。
CMS 采用的是标记 - 清除算法,并且是并发的。除了少数几个操作需要 Stop-the-world 之外,它可以在应用程序运行过程中进行垃圾回收。在并发收集失败的情况下,Java 虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。由于 G1 的出现,CMS 在 Java 9 中已被废弃[3]。
G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。
G1 能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是 G1 名字的由来。
即将到来的 Java 11 引入了 ZGC,宣称暂停时间不超过 10ms。如果你感兴趣的话,可参考 R 大的这篇文章[4]。
1.1 总结
1:二八法则-适用于许多的领域,对象在JVM对内存空间的生命周期也同样符合 ( 理想情况下,要进行Minor GC的时候,Eden 区中的对象基本都死亡了)
2:为了更好的JVM性能以及充分利用对象生命周期的二八法则,JVM的作者将JVM的对内存空间进行了分代的处理
3:堆内存空间=年轻代+老年代
年轻代=Eden+from+to
年轻代用于分配新生的对象
Eden-通常用于存储新创建的对象,对内存空间是共享的,所以,直接在这里面划分空间需要进行同步
from-当Eden区的空间耗尽时,JVM便会出发一次Minor GC 来收集新生代的垃圾,会把存活下来的对象放入Survivor区,也就是from区
注意,from和to是变动的
to-指向的Survivor区是空的,用于当发生Minor GC 时,存储Eden和from区中的存活对象,然后再交换from和to指针,以保证下一次Minor GC 时to指向的Survivor区还是空的。
老年代-用于存储存活时间更久的对象,比如:15次Minor GC 还存活的对象就放入老年代中
4:堆内存分代后,会根据他们的不同特点来区别对待,进行垃圾回收的时候会使用不同的垃圾回收方式,针对新生代的垃圾回收器有如下三个:Serial、Parallel Scavenge、Parallel New,他们采用的都是标记-复制的垃圾回收算法。
针对老年代的垃圾回收器有如下三个:Serial Old 、Parallel Old 、CMS,他们使用的都是标记-压缩的垃圾回收算法。
5:TLAB(Thread Local Allocation Buffer)-这个技术是用于解决多线程竞争堆内存分配问题的,核心原理是对分配一些连续的内存空间
6:卡表-这个技术是用于解决减少老年代的全堆空间扫描
2. 引用计数算法
在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的,也就是无用的,需要被回收的
引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但 它的原理简单,判定效率也很高,延迟要比后面介绍的tracing gc要低
内存不太充裕的地方使用引用计数仍然是个合理的选择,后面的tracing gc会要求更高的内存占用.
但是,在Java 领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单 的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数 就很难解决对象之间相互循环引用的问题
扩展: 更高级的引用计数实现会引入“弱引用”的概念来打破某些已知的循环引用
例子:
1 class A { 2 private B b; 3 public void setB(B b) { 4 this.b = b; 5 } 6 } 7 8 class B { 9 private A a = new A(); 10 public void setA(A a) { 11 this.a = a; 12 } 13 } 14 15 public void method() { 16 A a = new A(); 17 B b = new B(); 18 a.setB(b); 19 b.setA(a); 20 }
method方法中,执行完两个set后,method方法结束,对应的栈区的内存,也就是引用被回收,图中两条绿线引用消失,可以看到,留下两个对象在堆内存中循环引用,但此时已经没有地方在用他们了,造成内存泄漏
3. 可达性分析算法(Tracing GC)
3.1 概述
可达性分析算法可以解决引用计数法里面的循环引用问题。
虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。
比如说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。
误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。
3.2 Stop-the-world 以及安全点
怎么解决这个问题呢?在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。
Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。
这篇博客[2]还提到了一种比较另类的解释:安全词。一旦垃圾回收线程喊出了安全词,其他非垃圾回收线程便会一一停下。
当然,安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。
举个例子,当 Java 程序通过 JNI 执行本地代码时,如果这段代码不访问 Java 对象、调用 Java 方法或者返回至原 Java 方法,那么 Java 虚拟机的堆栈不会发生改变,也就代表着这段本地代码可以作为同一个安全点。
只要不离开这个安全点,Java 虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。
由于本地代码需要通过 JNI 的 API 来完成上述三个操作,因此 Java 虚拟机仅需在 API 的入口处进行安全点检测(safepoint poll),测试是否有其他线程请求停留在安全点里,便可以在必要的时候挂起当前线程。
除了执行 JNI 本地代码外,Java 线程还有其他几种状态:解释执行字节码、执行即时编译器生成的机器码和线程阻塞。阻塞的线程由于处于 Java 虚拟机线程调度器的掌控之下,因此属于安全点。
其他几种状态则是运行状态,需要虚拟机保证在可预见的时间内进入安全点。否则,垃圾回收线程可能长期处于等待所有线程进入安全点的状态,从而变相地提高了垃圾回收的暂停时间。
对于解释执行来说,字节码与字节码之间皆可作为安全点。Java 虚拟机采取的做法是,当有安全点请求时,执行一条字节码便进行一次安全点检测。
执行即时编译器生成的机器码则比较复杂。由于这些代码直接运行在底层硬件之上,不受 Java 虚拟机掌控,因此在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测。
那么为什么不在每一条机器码或者每一个机器码基本块处插入安全点检测呢?原因主要有两个。
第一,安全点检测本身也有一定的开销。不过 HotSpot 虚拟机已经将机器码中安全点检测简化为一个内存访问操作。在有安全点请求的情况下,Java 虚拟机会将安全点检测访问的内存所在的页设置为不可读,并且定义一个 segfault 处理器,来截获因访问该不可读内存而触发 segfault 的线程,并将它们挂起。
第二,即时编译器生成的机器码打乱了原本栈桢上的对象分布状况。在进入安全点时,机器码还需提供一些额外的信息,来表明哪些寄存器,或者当前栈帧上的哪些内存空间存放着指向对象的引用,以便垃圾回收器能够枚举 GC Roots。
由于这些信息需要不少空间来存储,因此即时编译器会尽量避免过多的安全点检测。
不过,不同的即时编译器插入安全点检测的位置也可能不同。以 Graal 为例,除了上述位置外,它还会在计数循环的循环回边处插入安全点检测。其他的虚拟机也可能选取方法入口而非方法出口来插入安全点检测。
不管如何,其目的都是在可接受的性能开销以及内存开销之内,避免机器码长时间不进入安全点的情况,间接地减少垃圾回收的暂停时间。
除了垃圾回收之外,Java 虚拟机其他一些对堆栈内容的一致性有要求的操作也会用到安全点这一机制。我会在涉及的时侯再进行具体的讲解。
3.3 垃圾回收的三种方式
垃圾回收的三种方式
当标记完所有的存活对象时,我们便可以进行死亡对象的回收工作了。主流的基础回收方式可分为三种。
第一种是清除(sweep),即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。
清除这种回收方式的原理及其简单,但是有两个缺点。一是会造成内存碎片。由于 Java 虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。
另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。
第二种是压缩(compact),即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。
第三种则是复制(copy),即把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。
当然,现代的垃圾回收器往往会综合上述几种回收方式,综合它们优点的同时规避它们的缺点。在下一篇中我们会详细介绍 Java 虚拟机中垃圾回收算法的具体实现。
3.3 可达性算法分析
当前主流的商用程序语言(包括Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是 通过可达性分析(Reachability Analysis)算法来判定对象是否存活的.
这个算法的基本思路就是通过 一系列称为“GC Roots”的根集合作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
可达性分析涉及到两个步骤, 1.根节点枚举和 2.查找引用链,现在我们主要说其中的查找引用链部分的内容
如下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。
通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
注意,是一组必须活跃的引用,不是对象。
1.所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
2.在方法区中类静态属性引用,譬如Java类的引用类型静态变量。
3.在方法区中的常量的引用,譬如字符串常量池(String Table)里的引用。
4.在本地方法栈中JNI(即通常所说的Native方法)中的引用。
5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
6.所有被同步锁(synchronized关键字)持有的对象的引用。
7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
3.1 如何高效找到GC Roots
有的读者可能会觉得很奇怪,我们在上一篇已经提到过, 固定可作为GC Roots的节点, 不就是 在全局性的引用(例如常量或类静态属性)与执行上下文(例如 栈帧中的本地变量表)中吗
我们不是已经知道GC Roots是哪些了吗?
实际上, 尽管目标明确,但查找过程要做到高效并非一件容易的事情, 知道它是什么,和能够精确的找到它,是两码事, 想要更快速的找到它,又是一码事,所以才有了今天的文章
现在Java应 用越做越庞大,光是方法区的大小就常有数百上千兆,里面的类、常量等更是车载斗量,如果在这日渐庞大的栈区和方法区中,逐地址寻找引用,对于这个应用的性能来说,一定会是个灾难.
现在可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发(具体见java如何判断哪些对象该被回收的文末),但根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行
这里“一致性”的意思是整个枚举期间执行子系统 看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化 的情况,若这点不能满足的话,分析结果准确性也就无法保证。
这个"被冻结在某个时间点上"就是后面要提到的主动式中断
这是导致垃圾收集过程必须停顿所有用户线程的其中一个重要原因(stop the word),即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、 ZGC等收集器,枚举根节点时也是必须要停顿的。
由于目前主流Java虚拟机使用的都是准确式垃圾收集
保守式 GC: 遍历方法区和栈区查找 ; 准确式 GC: 通过后文提到的称之为 OopMap 的数据结构来记录 GC Roots 的位置
所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。
在HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来。这样收集器在扫描时就可以直接得知这些信 息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。
下面代码是HotSpot虚拟机客户端模式下生成的一段String::hashCode()方法的本地代码,可 以看到在0x026eb7a9处的call指令有OopMap记录,它指明了EBX寄存器和栈中偏移量为16的内存区域 中各有一个普通对象指针(Ordinary Object Pointer,OOP)的引用,有效范围为从call指令开始直到 0x026eb730(指令流的起始位置)+142(OopMap记录的偏移量)=0x026eb7be,即hlt指令为止。
[Verified Entry Point] 0x026eb730: mov %eax,-0x8000(%esp) ………… ;; ImplicitNullCheckStub slow case 0x026eb7a9: call 0x026e83e0 ; OopMap{ebx=Oop [16]=Oop off=142} ; *caload ; - java.lang.String::hashCode@48 (line 1489) ; {runtime_call} 0x026eb7ae: push $0x83c5c18 ; {external_word} 0x026eb7b3: call 0x026eb7b8 0x026eb7b8: pusha 0x026eb7b9: call 0x0822bec0 ; {runtime_call} 0x026eb7be: hlt
3.2 安全点
在OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,如果为每一条指令都生成 对应的OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。
实际上HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)。
有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。
因此,安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过 分增大运行时的内存负荷。
安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准 进行选定的
HotSpot会在所有方法的临返回之前,以及所有非counted loop的循环的回跳之前放置安全点。
“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转 等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。
为什么把这些位置设置为jvm的安全点呢
主要目的就是避免程序长时间无法进入safepoint,比如JVM在做GC之前要等所有的应用线程进入到安全点后VM线程才能分派GC任务 ,如果有线程一直没有进入到安全点,就会导致GC时JVM停顿时间延长,比如下面的例题
对于安全点,另外一个需要考虑的问题是
如何在垃圾收集发生时让所有线程(这里其实不包括 执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。这个停顿正是根节点枚举期间,产生Stop The World 全局性停顿的原因
这里有两种方案可供选择:
主动式中断(Voluntary Suspension)和 抢先式中断 (Preemptive Suspension)
主动式中断
主动式中断的思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一 个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他 需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新 对象。
抢先式中断
而抢先式中断不需要线程的执行代码 主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
jvm大局观之内存管理篇: 理解jvm安全点,写出更高效的代码
例:Thread2是 counted loop,单次循环末尾不会被加入安全点,整个for循环期执行结束之前,都不会进入安全点程序长时间无法进入安全点
1 package thread; 2 3 public class TestBlockingThread { 4 5 static Thread t1 = new Thread(() -> { 6 while (true) { 7 long start = System.currentTimeMillis(); 8 try { 9 Thread.sleep(1000L); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 long cost = System.currentTimeMillis() - start; 14 //按照正常情况,t1线程,大致上应是每隔1000毫秒左右,会输出一句话 我们使用 cost 来记录实际等待的时间 15 //如果实际时间cost大于1010毫秒 我们就使用System.err输出,也就是红色字样的输出,否则则是正常输出 16 (cost > 1010L ? System.err : System.out).printf("thread: %s, costs %d ms\n", Thread.currentThread().getName(), cost); 17 } 18 }); 19 20 static Thread t2 = new Thread(() -> { 21 while (true) { 22 23 //下面是一个counted loop,单次循环末尾不会被加入安全点,整个for循环期执行结束之前,都不会进入安全点 24 //存在这样一种情况, 如果某次for循环才刚刚开始没多久, 因为内存过多而需要进行垃圾收集 25 //而我们知道,垃圾收集刚开始的时候需要先获取所有根节点,而根节点的获取依赖所有线程抵达安全点 26 //线程t1很简单,只需要隔1s就会进入安全点,之后,线程t1需要等到其他线程(t2)也进入到安全点 27 //而t2此时才刚刚是for循环的刚开始,所以需要消耗大量时间走完剩下的循环次数,这也就是为什么有时候t1实际cost时间多达5s的原因 28 //也就是gc发生时,要获取所有根节点,而想要获取根节点,就要所有线程抵达安全点,已经抵达的线程(t1)需要等待未抵达的线程(t2)到达安全点 然后才会继续垃圾收集的剩下内容 29 for (int i = 1; i <= 1000000000; i++) { 30 boolean b = 1.0 / i == 0; 31 } 32 33 try { 34 Thread.sleep(10); 35 } catch (InterruptedException e) { 36 e.printStackTrace(); 37 } 38 } 39 }); 40 41 private static final int _50KB = 50 * 1024; 42 43 //下面的代码在创建大量的对象, 一定会导致隔一段时间会出现垃圾收集 44 static Thread t3 = new Thread(() -> { 45 while (true) { 46 try { 47 Thread.sleep(5); 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } 51 byte[] bytes = new byte[_50KB]; 52 } 53 }); 54 55 public static void main(String[] args) throws InterruptedException { 56 t1.start(); 57 Thread.sleep(1500L); 58 t2.start(); 59 t3.start(); 60 } 61 }
结果:
thread: Thread-0, costs 1001 ms
thread: Thread-0, costs 5418 ms
thread: Thread-0, costs 1004 ms
thread: Thread-0, costs 1005 ms
thread: Thread-0, costs 1004 ms
thread: Thread-0, costs 1004 ms
thread: Thread-0, costs 1001 ms
thread: Thread-0, costs 1003 ms
thread: Thread-0, costs 4027 ms
thread: Thread-0, costs 1001 ms
thread: Thread-0, costs 3919 ms
thread: Thread-0, costs 1003 ms
thread: Thread-0, costs 1005 ms
thread: Thread-0, costs 1002 ms
thread: Thread-0, costs 1002 ms
thread: Thread-0, costs 1002 ms
thread: Thread-0, costs 4858 ms
thread: Thread-0, costs 1004 ms
thread: Thread-0, costs 1002 ms
4. 死亡标记与拯救
在可达性算法中不可达的对象,并不是“非死不可”的,要真正宣告一个对象死亡,至少要经历两次标记的过程。
如果对象在进行可达性分析之后,没有与GC Roots相连接的引用链,它会被第一次标记,并进行筛选,(第二次筛选)筛选的条件是此对象是否有必要执行finalize()方法。
执行finalize()方法的两个条件:
1、重写了finalize()方法。
2、finalize()方法之前没被调用过,因为对象的finalize()方法只能被执行一次。
如果满足以上两个条件,这个对象将会放置在F-Queue的队列之中,并在稍后由一个虚拟机自建的、低优先级Finalizer线程来执行它。
对象的“自我拯救”
finalize()方法是对象脱离死亡命运最后的机会,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联即可,比如把自己(this关键字)赋值给某个类变量或对象的成员变量。
来看具体的实现代码:
代码:
1 package java8; 2 3 /** 4 * 五. 死亡标记与拯救 5 * 在可达性算法中不可达的对象,并不是“非死不可”的,要真正宣告一个对象死亡,至少要经历两次标记的过程。 6 * 如果对象在进行可达性分析之后,没有与GC Roots相连接的引用链,它会被第一次标记,并进行筛选,筛选的条件是此对象是否有必要执行finalize()方法。 7 * 8 * 执行finalize()方法的两个条件: 9 * 1、重写了finalize()方法。 10 * 2、finalize()方法之前没被调用过,因为对象的finalize()方法只能被执行一次。 11 * 如果满足以上两个条件,这个对象将会放置在F-Queue的队列之中,并在稍后由一个虚拟机自建的、低优先级Finalizer线程来执行它。 12 * 13 * 对象的“自我拯救” 14 * 15 * finalize()方法是对象脱离死亡命运最后的机会,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联即可,比如把自己(this关键字)赋值给某个类变量或对象的成员变量。 16 * 17 * [执行的结果: 18 * 执行finalize方法 19 * 我还活着 20 * 我已经死了] 21 * 22 * finalize总结: 能够使得可达性算法中不可达的对象重新可达,但是由于finalize()只能执行一次以及不确定性大, 23 * 无法保证各个对象的调用顺序,已被官方明确声明为 不推荐使用的语法,建议大家完全可以忘掉Java语言里面的这个方法 24 * 25 * 26 */ 27 public class FinalizeDemo { 28 29 public static FinalizeDemo Hook = null; 30 31 @Override 32 protected void finalize() throws Throwable { 33 super.finalize(); // Finalizer线程执行,非主线程 34 System.out.println("执行finalize方法"); 35 FinalizeDemo.Hook = this; 36 } 37 38 public static void main(String[] args) throws InterruptedException { 39 Hook = new FinalizeDemo(); 40 // 第一次拯救 41 Hook = null; 42 System.gc(); 43 Thread.sleep(500); // 等待finalize执行 44 if (Hook != null) { 45 System.out.println("第一次:我还活着"); 46 } else { 47 System.out.println("第一次:我已经死了"); 48 } 49 // 第二次,代码完全一样 50 Hook = null; 51 System.gc(); 52 Thread.sleep(500); // 等待finalize执行 53 if (Hook != null) { 54 System.out.println("第二次:我还活着"); 55 } else { 56 System.out.println("第二次:我已经死了"); 57 } 58 } 59 }
非主线程执行finalize(),由Finalizer线程来执行它