以下是本系列博客参考的相关文档:
来源链接:https://juejin.im/post/5ef1fd01f265da02ab172c19
①、JDK1.7虚拟机规范:docs.oracle.com/javase/spec…
JDK1.8虚拟机规范:docs.oracle.com/javase/spec…
②、Oracle Java SE 8 产品组件:docs.oracle.com/javase/8/do…
③、周志明老师:《深入理解Java虚拟机:JVM高级特性与最佳实践》
④、陈涛老师:《HotSpot实战》
一、简介
1.Oracle Java SE 8 产品组件
通常来说 Java平台标准版(Java SE)包括 Java SE开发工具包(JDK)和Java SE运行时环境(JRE)。
JRE提供了运行以Java编程语言编写的applet和应用程序所必需的库,Java虚拟机和其他组件;JDK包括JRE以及编译器和调试器等命令行开发工具,可以用来开发Java应用程序 。
PS:JDK包含JRE,我们通常安装JDK的同时也会安装JRE。
2.虚拟机
上图的最下一行Java虚拟机是被 JRE 所包含,我们在介绍Java虚拟机时,先了解虚拟机的概念。
所谓虚拟机,其实就是一台虚拟的机器,可以用来执行一系列虚拟的命令。大体上虚拟机可以分为两种:系统虚拟机和程序虚拟机。
①、系统虚拟机:是完全对物理计算机的仿真,可以说和一台真实的PC操作系统没什么区别。比如常用的 Vmare 以及 Visual Box 软件,通过这些软件能够模拟出具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。
②、程序虚拟机:专门为执行单个计算程序而产生,最典型的就是Java虚拟机,在Java虚拟机中执行字节码文件命令。
3.Java虚拟机
了解了什么是虚拟机,我们再看什么是 Java虚拟机。
Java虚拟机可以看做是一台抽象的计算机,如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域,它与Java语言没有必然的联系,只与特定的二进制文件——class 文件格式关联(字节码文件),可以通过Java语言或者其他语言编写的程序编译成class文件,然后在Java虚拟机上运行。Java虚拟机有以下二个特点:
- 语言无关
- 平台无关
Java从诞生之初就宣传的一个口号:一次编写,到处运行。
也就是说Java是一个跨平台的语言,那么Java是如何实现跨平台的呢?
其实Java之所以跨平台是因为Java虚拟机的适配,不同的系统实现不同的Java虚拟机。Java虚拟机就相当于操作系统和应用程序之间的中介,每种平台安装适应该平台的Java虚拟机,那么我们编写的程序当然能够在任意平台运行。
4.Java虚拟机种类
商用虚拟机:
①、Sun HotSpot
该虚拟机性能优越,是 sun JDK1.3 及以后所有 sun JDK 版本默认的虚拟机,使用最为广泛,本系列博客就是以这个虚拟机为平台进行介绍。
②、BEA JRockit
JRockit 虚拟机是 BEA 公司于 2002 年从 Appeal Virtual Machines 收购获得的虚拟机。它是一款面向服务器硬件和服务端使用场景高度优化过得虚拟机,曾经号称是“世界上速度最快的虚拟机”。由于专注于服务端应用,它的内部不包含解析器的实现,全部代码都靠即时编译器编译后执行。
③、IBM J9
J9 虚拟机是 IBM 公司单独开发的高性能虚拟机,它并不独立出售,而是作为 IBM 公司各种产品的执行平台,IBM 把它定义为一个可以适应从嵌入式设备到大型企业级应用的、高可移植性的Java运行平台。
④、Sun Classic
这个虚拟机很原始,是 JDK1.0 时代使用的Java虚拟机,是各种虚拟机的鼻祖,它的内部不存在即时编译器,只能使用纯解释的方式运行。
⑤、Sun Exact
这是 Sun 公司在 HotSpot 之外的另一个虚拟机,在 JDK1.2 时代曾短暂的投入过商用,它和 HotSpot 同时开发,但最终被 HotSpot 取代。
⑥、Apache Harmony
Harmony 是 Apache 软件基金会主导的、开源的、独立的、实际兼容与 JDK1.5 和 JDK1.6的虚拟机实现,它间接催生了 Google Android 平台的 Dalvik 虚拟机,Android 的影响力现在有多大不用多说,目前已经是最成功的的数码设备通用平台。但是由于它的 TCK 授权问题,直接导致 Apache 与 Oracle 的决裂,从而退出了 JCP 组成,这是近代 Java 阵营遇到的最严重的分裂危机。
嵌入式虚拟机
①、Dalvik
Dalvik 虚拟机是 Google 等厂商合作开发的 Android 移动设备平台的核心组成部分之一,它执行 dex(Dalvik Executable) 文件而不是 class 文件,使用寄存器架构而不是栈架构,但是它的开发体系与Java有着千丝万缕的关系,可以直接使用大部分的 Java API、dex 文件可以直接从class文件转化而来。并且在 Android 2.2 中提供了即时编译器的实现,性能大大的提高。
②、KVM
在 Android、IOS 等智能手机操作系统出现之前,曾广泛应用于手机平台的一种虚拟机。
③、CDC/CLDC HotSpot
CDC和 CLDC HotSpot 分别是 Sun 针对高端嵌入式设备和中低端嵌入式设备的虚拟机,用来代替 KVM。
二、运行时内存结构
首先通过一张图了解 Java程序的执行流程:
我们编写好的Java源代码程序,通过Java编译器javac编译成Java虚拟机识别的class文件(字节码文件),然后由 JVM 中的类加载器加载编译生成的字节码文件,加载完毕之后再由 JVM 执行引擎去执行。在加载完毕到执行过程中,JVM会将程序执行时用到的数据和相关信息存储在运行时数据区(Runtime Data Area),这块区域也就是我们常说的JVM内存结构,垃圾回收也是作用在该区域。
关于这幅图涉及到的:
①、class文件
②、类加载器
③、运行时数据区
④、执行引擎
⑤、垃圾回收器
这都是接下来将要介绍的重点。
本篇博客我们将首先介绍什么是运行时数据区。
PS:下面介绍的是根据 Java虚拟机规范 定义的运行时数据区,上一篇博客我们讲过根据虚拟机规范实现的虚拟机有很多个,而不同的虚拟机其运行时数据区定义也会有所不同。比如默认的 HotSpot 在实现 JDK1.7 虚拟机规范时,其常量池的定义不在方法区中,而是移到了堆中;到了 HotSpot JDK1.8 中,则彻底移除了持久代(方法区)而使用Metaspace(元数据区)来进行替代等等,关于这些区别本篇博客也会在文章末尾进行相应的说明。
1.运行时数据区结构图
①、Java虚拟机规范定义的运行时数据区
②、HotSpot JDK1.8定义的运行时数据区
注意:HotSpot实现的运行时数据区和Java虚拟机规范定义的还是有所不同的,
①、将Java虚拟机栈和本地方法栈合二为一;
②、元数据区取代了方法区,并且元数据区不在Java虚拟机中,而是在本地内存中。
③、运行时常量池由方法区中移到了堆中
2.程序计数器
程序计数器(Program Conputer Register)这是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
①、线程私有
Java虚拟机支持多线程,是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任一确定的时刻,一个处理器只会执行一条线程中的指令,因此为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。因此线程启动时,JVM 会为每个线程分配一个PC寄存器(Program Conter,也称程序计数器)。
②、记录当前字节码指令执行地址
如果当前线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,则这个计数器值为空(Undefined)。
③、不抛 OutOfMemoryError 异常
程序计数器的空间大小不会随着程序执行而改变,始终只是保存一个 returnAdress 类型的数据或者一个与平台相关的本地指针的值。所以该区域是Java运行时内存区域中唯一一个Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
3.虚拟机栈
Java虚拟机栈(Java Virtual Machine stack),这块区域也是线程私有的,与线程同时创建,用于存储栈帧。Java 每个方法执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
①、线程私有
随线程创建而创建,声明周期和线程保持一致。
②、由栈帧组成
线程每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
③、抛出 StackOverflowError 和 OutOfMemoryError 异常
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。
4.本地方法栈
本地方法栈(Native Method Stacks)作用和虚拟机栈类型,虚拟机栈执行的是Java方法,本地方法栈执行的是 Native 方法,本地方法栈也会抛出抛出 StackOverflowError 和 OutOfMemoryError 异常。
注意:由于虚拟机规范并没有对本地方法栈中的方法使用语言、使用方式和数据结构强制规定,因此具体的虚拟机可以自由实现它。上图我们也给出在 HotSpot 虚拟机中,本地方法栈和虚拟机栈合为一体了。
5.Java堆
Java堆是Java虚拟机所管理内存最大、被所有线程共享的一块区域,目的是用来存放对象,基本上所有的对象实例和数组都在堆上分配(不是绝对)。Java堆也是垃圾回收器管理的主要区域。
①、线程共享
堆存放的对象,某个线程修改了对象属性,另外一个线程从堆中获取的该对象是修改后的对象,为什么堆要设计成线程共享呢?
我们可以假设堆是线程私有的,很显然一个系统创建的对象会有很多,而且有些对象会比较大,如果设计成线程私有的,那么如果有很多线程同时工作,那么都必须给他们分配相应的私有内存,我相信内存很快就撑爆了,很显然将堆设计为线程共享是最好不过了,不过凡事都具有两面性,线程共享的设计这也带来了多线程并发资源冲突问题,关于这个问题由于不是本系列博客的主旨,这里就不做详细介绍了。
②、存放对象
基本上所有的对象实例和数组都要在堆上进行分配,但是随着 JIT 编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换等优化技术会导致对象不一定在堆上进行分配。
③、垃圾收集
Java堆也被称为“GC堆”,是垃圾回收器的主要操作内存区域。当前垃圾回收器都是使用的分代收集算法,所以Java堆还可以分为:新生代和老年代,而新生代又可以分为 Eden 空间、From Survivor 空间、To Survivor空间。这是为了更好的回收内存,关于垃圾回收算法在后续博客会详细介绍。
④、抛出 OutOfMemoryError 异常
根据Java虚拟机规范,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,实现时既可以实现成固定大小,也可以是扩展的。如果在堆中没有完成实例分配,并且堆也无法扩展,将抛出OutOfMemoryError 异常。
6.方法区
方法区(Method Area)用来存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区也称为“永久代”,这是因为垃圾回收器对方法区的垃圾回收比较少,主要是针对常量池的回收以及对类型的卸载,回收条件比较苛刻。经常会导致对此内存未完全回收而导致内存泄露,最后当方法区无法满足内存分配时,将抛出 OutOfMemoryError 异常。
PS:在Java虚拟机规范中把方法区描述为堆的一个逻辑部分(docs.oracle.com/javase/spec… J9等虚拟机不存在永久代的概念)。
在JDK1.8 的 HotSpot 虚拟机中,已经去掉了方法区的概念,用 Metaspace 代替,并且将其移到了本地内存来规划了。
7. 运行时常量池
在Java虚拟机规范中,运行时常量池(Runtime Constant Pool)用于存放编译期生成的各种字面量和符号引用,是方法区的一部分。但是Java虚拟机规范对其没有做任何细节的要求,所以不同虚拟机实现商可以按照自己的需求来实现该区域,比如在 HotSpot 虚拟机实现中,就将运行时常量池移到了堆中。
①、存放字面量、符号引用、直接引用
通常来说,该区域除了保存Class文件中描述的引用外,还会把翻译出来的直接引用也存储在运行时常量池,并且Java语言并不要求常量一定只能在编译器产生,运行期间也可能将常量放入池中,比如String类的intern()方法,当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中,并返回此对象的引用。关于该方法的介绍可以看我这篇博客。
②、抛出 OutOfMemoryError 异常
运行时常量池是方法区的一部分,会受到方法区内存的限制,当常量池无法申请到内存时,会抛出该异常。
8.直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,它也不是Java虚拟机规范定义的内存区域。我们可以看到在 HotSpot 中,就将方法区移除了,用元数据区来代替,并且将元数据区从虚拟机运行时数据区移除了,转到了本地内存中,也就是说这块区域是受本机物理内存的限制,当申请的内存超过了本机物理内存,才会抛出 OutOfMemoryError 异常。
直接内存也是受本机物理内存的限制,在JDK1.4中新加入的 NIO(new input/output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用操作,这样避免了在Java堆和Native堆中来回复制数据,显著提高性能。
三、垃圾回收
如果对C++这门语言熟悉的人,再来看Java,就会发现这两者对垃圾(内存)回收的策略有很大的不同。
C++:垃圾回收很重要,我们必须要自己来回收!!!
Java:垃圾回收很重要,我们必须交给系统来帮我们完成!!!
我想这也能看出这两门语言设计者的心态吧,总之,Java和C++之间有一堵由内存动态分布和垃圾回收技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
本篇博客我们就来详细介绍Java的垃圾回收策略。
1.为什么要进行垃圾回收
我们知道Java是一门面向对象的语言,在一个系统运行中,会伴随着很多对象的创建,而这些对象一旦创建了就占据了一定的内存,在上一篇博客Java运行时内存结构中,我们介绍过创建的对象是保存在堆中的,当对象使用完毕之后,不对其进行清理,那么会一直占据内存空间,很明显内存空间是有限的,如果不回收这些无用的对象占据的内存,那么新创建的对象申请不了内存空间,系统就会抛出异常而无法运行,所以必须要经常进行内存的回收,也就是垃圾收集。
2.为什么要了解垃圾回收
文章开头,我们就说Java的垃圾回收是系统自动进行的,不需要我们程序员手动处理,那么我们为什么还要了解垃圾回收呢,?
其实这也是一个程序员进阶的过程,生产项目在运行过程中,很可能会存在内存溢出、内存泄露等问题,出现了这些问题,我们应该怎么排查?以及在生产服务器有限的资源上如何更好的分配Java运行时内存区域,提高系统运行效率等,我们必须知其然知其所以然。
PS:本篇博客只是介绍Java垃圾回收机制,关于排查内存泄漏、溢出,运行时内存区域参数调优等会在后面进行介绍。
3.回收哪部分区域内存
还是结合上一篇博客Java运行时内存结构,我们介绍了Java运行时的内存结构,其中程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作,这几个区域的内存分配和回收都具备确定性,在方法结束或线程结束时,内存也就跟着回收了,所以不需要我们考虑。
那么现在就剩下Java堆和方法区了,这两块区域在编译期间我们并不能完全确定创建多少个对象,有些是在运行时期创建的对象,所以Java内存回收机制主要是作用在这两块区域。
4.如何判断对象为垃圾对象
通过上面介绍了,我们了解了为什么要进行垃圾回收以及回收哪部分的垃圾,那么接下来我们怎么去区分哪些对象为垃圾呢?
换句话来说,我们如何判断哪些对象还“活着”,哪些对象已经“死了”,那些“死了”的对象占据的内存就是我们要进行回收的。
①、引用计数算法
这种算法是这样的:给每一个创建的对象增加一个引用计数器,每当有一个地方引用它时,这个计数器就加1;而当引用失效时,这个计数器就减1。当这个引用计数器值为0时,也就是说这个对象没有任何地方在使用它了,那么这就是一个无效的对象,便可以进行垃圾回收了。
这种算法实现简单,而且效率也很高。但是Java没有采用该算法来进行垃圾回收,因为这种算法无法解决对象之间的循环引用问题。
下面我们就来构造一个循环引用的例子:
首先,有一个 Person 类,这个类有两个自引用属性,分别表示其父亲,儿子。
package com.ys.algorithmproject.leetcode.demo.JVM;
/**
* Create by YSOcean
*/
public class Person {
private Byte[] _1MB = null;
public Person() {
/**
* 这个成员属性的作用纯粹就是占据一定内存,以便在日志中查看是否被回收
*/
_1MB = new Byte[1*1024*1024];
}
private Person father;
private Person son;
public Person getFather() {
return father;
}
public void setFather(Person father) {
this.father = father;
}
public Person getSon() {
return son;
}
public void setSon(Person son) {
this.son = son;
}
}
接着,我们通过Person类构造两个对象,分别是父亲,儿子,如下:
public static void main(String[] args) {
Person father = new Person();
Person son = new Person();
father.setSon(son);
son.setFather(father);
father = null;
son = null;
/**
* 调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,
* 而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
*/
System.gc();
}
首先,从第3-6行代码,其运行时内存结构图如下:
father对象和son对象,其引用计数第一个是栈内存指向,第二个就是其属性互相引用对方,所有引用计数器都是2。
接着我们看第8,9行代码,分别将这两个对象置为null,也就是去掉了栈内存指向。
这时候其实这两个对象只是自己互相引用了,没有别的地方在引用它们,引用计数器为1,那么这两个对象按照引用计数算法实现的虚拟机就不会回收,可想而知,这是我们不能接受的。
所以Java虚拟机都没有使用该算法来判断对象是否存活,我们可以通过增加打印虚拟机参数来验证。
我们将上面的man函数,增加如下Java虚拟机参数,用来打印gc信息。
-verbose:gc
在IDEA编辑器中,添加方式如下:
运行结果如下:
我们看到12201K->1088K(125952K)的输出,表示垃圾收集GC前有12201K,回收后剩下1088K,堆的总量为125952K,回收的内存为12201K-1088K = 11113K。
换句话说,上面的例子Java虚拟机是有进行垃圾回收的,所以,这也间接佐证了Java虚拟机并不是采用的引用计数法来判断对象是否是垃圾。
PS:这些参数信息详解也会在后面博客进行详细介绍。
②、根搜索算法
我们这里直接给出结论:在主流的商用程序中(Java,C#),都是使用根搜索算法(GC Roots Tracing)来判定对象是否存活。
该算法思路:通过一系列名为“GC Roots” 的对象作为终点,当一个对象到GC Roots 之间无法通过引用到达时,那么该对象便可以进行回收了。
上图Object1,Object2,Object3,Object4到GC Roots是可达的,所以不会被作为垃圾回收。
上图Object1,Object2,Object3这三个对象互相引用,但是到 GC Roots不可达,所以都会被垃圾回收掉。
那么有哪些对象可以作为 GC Roots 呢?
在Java语言中,有如下4中对象可以作为 GC Roots:
PS:红色的对象是要被当做垃圾回收的!
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中的静态变量属性引用的对象
方法区中常量引用的对象
本地方法栈中(JNI)(即一般说的Native方法)的引用的对象
5.如何进行垃圾回收
垃圾回收涉及到大量的程序细节,而且各个平台的虚拟机操作内存的方式也不一样,但是他们进行垃圾回收的算法是通用的,所以这里我们也只介绍几种通用算法。
①、标记-清除算法
算法实现:分为标记-清除两个阶段,首先根据上面的根搜索算法标记出所有需要回收的对象,在标记完成后,然后在统一回收掉所有被标记的对象。
缺点:
1、效率低:标记和清除这两个过程的效率都不高。
2、容易产生内存碎片:因为内存的申请通常不是连续的,那么清除一些对象后,那么就会产生大量不连续的内存碎片,而碎片太多时,当有个大对象需要分配内存时,便会造成没有足够的连续内存分配而提前触发垃圾回收,甚至直接抛出OutOfMemoryExecption。
②、复制算法
为了解决标记-清除算法的两个缺点,复制算法诞生了。
算法实现:将可用内存按容量划分为大小相等的两块区域,每次只使用其中一块,当这一块的内存用完了,就将还活着的对象复制到另一块区域上,然后再把已使用过的内存空间一次性清理掉。
优点:每次都是只对其中一块内存进行回收,不用考虑内存碎片的问题,而且分配内存时,只需要移动堆顶指针,按顺序进行分配即可,简单高效。
缺点:将内存分为两块,但是每次只能使用一块,也就是说,机器的一半内存是闲置的,这资源浪费有点严重。并且如果对象存活率较高,每次都需要复制大量的对象,效率也会变得很低。
作者:IT可乐
链接:https://juejin.im/post/5ef1fd01f265da02ab172c19
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
③、标记-整理算法
上面我们说过复制算法会浪费一半的内存,并且对象存活率较高时,会有过多的复制操作,效率低下。
如果对象存活率很高,基本上不会进行垃圾回收时,标记-整理算法诞生了。
算法实现:首先标记出所有存活的对象,然后让所有存活对象向一端进行移动,最后直接清理到端边界以外的内存。
局限性:只有对象存活率很高的情况下,使用该算法才会效率较高。
④、分代收集算法
当前商业虚拟机都是采用此算法,但是其实这不是什么新的算法,而是上面几种算法的合集。
算法实现:根据对象的存活周期不同将内存分为几块,然后不同的区域采用不同的回收算法。
1、对于存活周期较短,每次都有大批对象死亡,只有少量存活的区域,采用复制算法,因为只需要付出少量存活对象的复制成本即可完成收集;
2、对于存活周期较长,没有额外空间进行分配担保的区域,采用标记-整理算法,或者标记-清除算法。
比如,对于 HotSpot 虚拟机,它将堆空间分为如下两块区域:
堆有新生代和老年代两块区域组成,而新生代区域又分为三个部分,分别是 Eden,From Surivor,To Survivor ,比例是8:1:1。
新生代采用复制算法,每次使用一块Eden区和一块Survivor区,当进行垃圾回收时,将Eden和一块Survivor区域的所有存活对象复制到另一块Survivor区域,然后清理到刚存放对象的区域,依次循环。
老年代采用标记-清除或者标记-整理算法,根据使用的垃圾回收器来进行判断。
至于为什么要这样,这是由于内存分配的机制导致的,新生代存的基本上都是朝生夕死的对象,而老年代存放的都是存活率很高的对象。关于内存分配下篇博客我们会详细进行介绍。
6.何时进行垃圾回收
理清了什么是垃圾,怎么回收垃圾,最后一点就是Java虚拟机何时进行垃圾回收呢?
程序员可以调用 System.gc()方法,手动回收,但是调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
其次虚拟机会自行根据当前内存大小,判断何时进行垃圾回收,比如前面所说的,新生代满了,新产生的对象无法分配内存时,便会触发垃圾回收机制。
这里需要说明的是宣告一个对象死亡,至少要经历两次标记,前面我们说过,如果对象与GC Roots 不可达,那么此对象会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆盖 finalize()方法,或者该方法已经执行了一次,那么虚拟机都将视为没有必要执行finalize()方法。
如果这个对象有必要执行 finalize() 方法,那么该对象将会被放置在一个有虚拟机自动建立、低优先级,名为 F-Queue 队列中,GC会对F-Queue进行第二次标记,如果对象在finalize() 方法中成功拯救了自己(比如重新与GC Roots建立连接),那么第二次标记时,就会将该对象移除即将回收的集合,否则就会被回收。
链接:https://juejin.im/post/5ef1fd01f265da02ab172c19
可以微信搜索「 IT可乐 」第一时间阅读