全网最全JVM知识点速查

JVM

执行引擎

执行引擎主要包括什么?

解释器
分析器(JIT)
垃圾回收器

说一下你对解释执行与编译执行的理解

编译执行:
先编译成机器码文件再执行。
启动需要进行编译操作,启动效率低。
执行效率高。
会生成机器码文件,占用内存。
解释执行:
将代码一句一句地解释,过程中不会产生机器码文件。
启动效率高。
执行效率低。
不会生成机器码文件。

说一下你对解释器与JIT编译器的理解

解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

JIT (Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

jdk1.4之前是纯解释执行,后加入JIT,将热点代码机器指令缓存起来,重复调用时直接执行。

Java代码编译与执行的过程

橙色部分为javac编译过程,绿色部分为解释执行,蓝色部分为编译执行。
在这里插入图片描述

既然HotSpotVM中已经内置JIT编译器了,为什么还需要使用 解释器?"拖累"性能啊?

当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。响应速度快。
并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。

热点代码与探测方式

一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为oSR (On StackReplacement)编译。

目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge counter) 。
方法调用计数器用于统计方法的调用次数。
回边计数器则用于统计循环体执行的循环次数。

了解静态提前编译器(AOT编译器,Ahead Of Time Compiler)么?

Java9引入了静态提前编译器。AOT直接把 .java文件编译成本地机器代码的过程。

类的加载

什么是类加载器

类加载器子系统负责从文件系统或者网络中加载class文件,加载的类信息存放在方法区中。
JVM只支持两种类型的类加载器,分别为引导类加载器(BootStrapClassLoader)和 自定义加载器(User-Defined ClassLoader)。JVM规范将派生于抽象类ClassLoader的类加载器都划分为自定义类加载器,包括ExtensionClassLoader 和 ApplicationClassLoader。
BootstrapClassLoader是使用C/C++语言编写的,其他的自定义加载器是Java语言实现的。
Java的核心类库都是使用引导类加载器进行加载的,比如String。

引导类加载器(BootStrap ClassLoader)

C/C++语言实现,用来加载Java的核心类库(jre/lib/rt.jar、resource.jar、或sun.boot.class.path路径下的内容)。
加载扩展类和应用类加载器,并指定为他们的父类加载器。
出于安全考虑,Bootstrap ClassLoader只加载包名为java、javax、sun等开头的类。

虚拟机自带的加载器

扩展类加载器(Extension ClassLoader)

由Java语言编写,派生于ClassLoader类。
父类加载器为Bootstrap ClassLoader。
从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

应用程序加载器(Application ClassLoader)

由Java语言编写,派生于ClassLoader类。
父类加载器为Extension ClassLoader。
它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库。
该类加载是程序中默认的类加载器,通过ClassLoader#getSystemclassLoader ()方法可以获取到该类加载器。

引导类加载器加载哪些类?扩展类加载器加载哪些类?

import sun.security.ec.CurveDB;
import java.net.URL;
import java.util.Properties;
/**
 * @program: draft
 * @description: 加载路径
 * @author: atong
 * @create: 2021-09-24 23:38
 */
public class LoadPath {
    public static void main(String[] args) {
        System.out.println("************启动类加载器************");
        //获取BootstrapCLassLoader能够加载的api的路径
        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urLs) {
            System.out.println(element.toExternalForm());
        }
        System.out.println("************查看类加载器-引导类加载器************");
        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:
        ClassLoader classLoader = String.class.getClassLoader();
        System.out.println(classLoader);

        System.out.println("************扩展类加载器************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }
        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:
        System.out.println("************查看类加载器-扩展类加载器************");
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);
    }
}

result:

************启动类加载器************
file:/D:/Java/jdk1.8.0_231/jre/lib/resources.jar
file:/D:/Java/jdk1.8.0_231/jre/lib/rt.jar
file:/D:/Java/jdk1.8.0_231/jre/lib/sunrsasign.jar
file:/D:/Java/jdk1.8.0_231/jre/lib/jsse.jar
file:/D:/Java/jdk1.8.0_231/jre/lib/jce.jar
file:/D:/Java/jdk1.8.0_231/jre/lib/charsets.jar
file:/D:/Java/jdk1.8.0_231/jre/lib/jfr.jar
file:/D:/Java/jdk1.8.0_231/jre/classes
************查看类加载器-引导类加载器************
null
************扩展类加载器************
D:\Java\jdk1.8.0_231\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
************查看类加载器-扩展类加载器************
sun.misc.Launcher$ExtClassLoader@63947c6b

为什么要自定义类加载器?

隔离加载类:比如引入中间件jar包与本地类文件名相同,就需要隔离。
修改类加载的方式
扩展加载源
防止源码泄漏

如何自定义类加载器?

继承抽象类java.lang.ClassLoader,实现findclass ()方法。
在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承ClassLoader的子类URLClassLoader,这样就可以避免自己去编写findclass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

如何获取ClassLoader

public class ObtainClassLoader {
    public static void main(String[] args) {
        try {
            ClassLoader classLoader = Class.forName("java.lang.String")
            .getClassLoader();
            System.out.println(classLoader);

            ClassLoader classLoader1 = Thread.currentThread( ).getContextClassLoader();
            System.out.println(classLoader1);

            ClassLoader classLoader2 = ClassLoader.getSystemClassLoader( ).getParent();
            System.out.println(classLoader2);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

讲一下类的加载过程

加载(Loading):

将class文件以二进制流的形式读取到内存,并在内存中生成一个代表这个类的java.lang.Class对象。
BootStrapClassLoader(引导) – ExtensionClassLoader(扩展) – ApplicationClassLoader(系统)

链接(Linking):

Verify -> Prepare -> Resolve

验证(Verify)

文件格式验证(CA FE BA BE)、元数据验证、字节码验证、符号引用验证

准备(Prepare)

类变量分配内存,并默认初始值,即零值。
注意:不包括final修饰的static变量,因为final在编译的时候就会分配,准备阶段会显式初始化

解析(Resolve)

将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。

初始化(Initialization):

初始化阶段就是执行类构造器方法()的过程。此方法是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
如果类中没有静态变量则不会生成()方法。
若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。

什么是双亲委派机制?

Java虚拟机对class文件采用的是按需加载的方式,当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,是一种任务委派模式。

双亲委派机制的原理是什么?

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

在这里插入图片描述

双亲委派机制的优势

避免类的重复加载
保护程序安全,防止核心API被随意篡改,比如自己建一个java.lang.String

什么是沙箱安全机制?

自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java \lang \string.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
如下代码:
注意:package java.lang

package java.lang;

/**
 * @program: draft
 * @description: 自定义String
 * @author: atong
 * @create: 2021-10-08 21:30
 */
public class String {
    public static void main(String[] args) {
        System.out.println("Hello,String");
    }
}

运行结果如下:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

Process finished with exit code 1

内存管理

说一下JVM内存区域

栈是运行时单位,堆是存储单位。
在这里插入图片描述

在这里插入图片描述

程序计数器(Program Counter Register)

它可以看作是当前线程所执行的字节码的行号指示器。存储指向下一条指令的地址,由执行引擎读取下一条指令。
运行速度最快的存储区域。
JVM中的pc寄存器是对物理pc寄存器的一种抽象模拟。
唯一一个在JVM规范中没有规定OutOfMemoryError情况的区域。

虚拟机栈(VM Stacks)

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧。生命周期和线程一致。
是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。
不管使用哪种方式,都会导致栈帧被弹出。

栈中可能出现的异常

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。

StackOverflowError

如果采用固定大小的Java虚拟机栈,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出StackOverflowError异常。

OOM

如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出OutofMemoryError异常。

设置栈内存大小

-Xss 来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

栈帧的内部结构

在这里插入图片描述

局部变量表(Local Variables)

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。

局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小。

关于 slot 的理解

局部变量表,最基本的存储单元是slot(变量槽)。
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
byte 、 short、char、boolean在存储前被转换为int,long 和 double 占两个slot。

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域(比如出了大括号),那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

关于成员变量与局部变量的区别

成员变量会初始化默认值。
局部变量使用前,必须显示赋值,否则编译不通过。

操作数栈(Operand Stack)

操作数栈是由数组实现,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。

32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度

动态链接(指向运行时常量池的方法引用)

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

为什么需要常量池?

提供一些符号和常量,便于指令的识别。可以数据共享,节省内存。

方法返回地址(Return Address)

存放调用该方法的PC寄存器的值,即调用该方法指令的下一条指令的地址。
注意:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。

栈顶缓存技术(Top-of-StackCashing)扩展

基于栈式架构的虚拟机所使用的零地址指令更加紧凑,指令较多。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,Hotspot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack Cashing)技术,将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

方法中的局部变量是线程安全的么?

不一定,比如,StringBuild 入参或者返回值。扩展:逃逸分析

本地方法栈(Native Method Stacks)

与虚拟机栈所发挥的作用是非常相似的,本地方法栈是为虚拟机使用到的本地(Native)方法服务。
在HotSpot虚拟机中,直接将本地方法栈和虚拟机栈合二为一。

堆(Java Heap)

唯一目的就是存放对象实例,Java里“几乎”所有的对象实例都在这里分配内存。

《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

所有的线程共享Java堆,但是还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer,TLAB)。

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

JDK1.7 堆内存模型

在这里插入图片描述

年轻代与老年代

Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)。
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做Survivorfrom区、 Survivorto区)。

在Hotspot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1。
可以通过 -XX:SurvivorRatio=8 参数设置占比。(默认值是8)

几乎所有的对象都是在Eden取被创建的,注意,大对象直接进入老年代。

**-Xmn:**设置新生代内存空间大小(一般不设置,因为跟-XX:NewRatio参数有点冲突)

设置堆内存大小

-Xms 用于表示堆区的起始内存,等价于 -XX: InitialHeapsize
-Xmx 用于表示堆区的最大内存,等价于 -XX:MaxHeapsize
注意:
-X 是 jvm的运行参数
ms 是 memory start

默认情况下,初始内存大小:物理电脑内存大小/64。
最大内存大小是物理电脑内存大小/4。

生产环境中,建议将初始堆内存大小和最大堆内存设置成相同的值。避免扩容产生的GC。

配置新生代与老年代在堆结构的占比

-XX:NewRatio=2 (表示新生代占1,老年代占2,新生代占整个堆区的1/3,这是默认设置)
-XX:NewRatio=4(表示新生代占1,老年代占4,新生代占整个堆区的1/5)

堆空间参数设置

-XX:+PrintFlagsInitial:查看所有的参数的默认初始值
-XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
-Xms:初始堆空间内存﹑(默认为物理内存的1/64)
-Xmx:最大堆空间内存(默认为物理内存的1/4)
-Xmn:设置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代与老年代在堆结构的占比
-XX: survivorRatio:设置新生代中Eden和Se/s1空间的比例
-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
-XX :+PrintGCDetails:输出详细的GC处理日志
-XX:HandlePromotionFailure:是否设置空间分配担保

方法区(Method Area)

尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。但对于HotspotJVM而言,方法区还有一个别名叫做Non-Heap (非堆),目的就是要和堆分开。

方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.outofMemoryError:PermGen space(jdk1.7及之前)或者 java.lang.OutOfMemoryError: Metaspace(jdk1.8)

方法区存储什么?

它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  • 这个类型的完整有效名称(全名=包名.类名)
  • 这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
  • 这个类型的修饰符(public,abstract,final的某个子集)
  • 这个类型实现的接口的有序列表(比如某个类实现了很多个接口,这些接口的有序列表)
域信息

域的相关信息包括:域名称、域类型、域修饰符(public, private,protected,static,final,volatile,transient的某个子集)

方法信息
  • 方法名称
  • 方法的返回类型(或void)
  • 方法参数的数量和类型(按顺序)
  • 方法的修饰符
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
  • 异常表(abstract和native方法除外,记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引)
方法区大小设置
jdk7及以前

通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M
-XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
当JVM加载的类信息容量超过了这个值,会报异常outOfMemoryError : PermGen space

jdk8

与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常outOfMemoryError: Metaspace

-XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的
-XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspacesize时,适当提高该值。如果释放空间过多,则适当降低该值。

如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地Gc ,建议将-XX:Metaspacesize设置为一个相对较高的值。

方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

判断一个类是否可以回收,需要同时满足下面三个条件

该类所有的实例都已经回收,包括其子类实例。
加载该类的类加载器已经被回收。
该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾回收

垃圾回收概述

什么是垃圾?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

垃圾回收相关算法

对象是否存活方式

引用计数算法

引用计数算法(Reference Counting),对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。Python使用的就是引用计数来确认需要回收的对象。

优点

实现简单,垃圾对象便于辨识。
判定效率高,回收没有延迟性。

缺点

它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销。
引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致Java的垃圾回收器中没有使用该算法。如下图,若让obj1跟obj2指向null,则会导致内存泄漏。(两个对象互相引用,无法回收)
在这里插入图片描述

可达性分析算法

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

实现思路

可达性分析算法是以根对象集合(Gc Roots:活跃的引用)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。

使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)

如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。

在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

Java中GC Roots 包括几类元素?

虚拟机栈中引用的对象

本地方法栈内JNI(通常说的本地方法)引用的对象

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

方法区中常量引用的对象

所有被同步锁synchronized持有的对象

Java虚拟机内部的引用:基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。

反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

对象的finalization机制

Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。

当垃圾回收器发现没有引用指向一个对象,即回收此对象之前,总会先调用这个对象的finalize()方法。

finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用

从功能上来说,finalize()方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize ()方法在本质上不同于c++中的析构函数。

由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态

对象的三种状态

由于finalize()方法的存在,对象可以分为三种状态。

  • 可触及的:从根节点开始,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize ()中复活。
  • 不可触及的:对象的finalize ()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize ()只会被调用一次。只有在对象不可触及时才可以被回收。
对象回收的具体过程

判定一个对象objA是否可回收,至少要经历两次标记过程:

1.如果对象objA到 GC Roots没有引用链,则进行第一次标记。

2.进行筛选,判断此对象是否有必要执行finalize ()方法

  • 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
  • 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
  • finalize()方法是对象逃脱死亡的最后机会,稍后cc会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次

垃圾清除算法

目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法( Mark-sweep )、复制算法( copying )、标记–压缩算法( Mark-Compact )。

标记-清除(Mark - Sweep )算法

该算法被J.McCarthy等人在1960年提出并应用于Lisp语言。

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

  • 标记:collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  • 清除:collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
优点

比较基础,比较常见,简单

缺点

效率不算高(标记与清楚都需要O(n)级别的操作)。

在进行GC的时候,需要停止整个应用程序(STW),导致用户体验差。

这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表(占用内存空间)。

清除原理

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

复制算法

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点

没有标记和清除过程,实现简单,运行高效。
复制过去以后保证空间的连续性,不会出现“碎片”问题。

缺点

此算法的缺点也是很明显的,就是需要两倍的内存空间。
对象存储位置变化,导致地址变化,需要维护对象引用关系,开销大。

注意

如果系统中的存活对象很多(垃圾对象很少),复制算法不会很理想。
考虑极限状态,全是存活对象。垃圾回收时,发现什么垃圾都没回收掉,还平白无故复制了一份,简直就是浪费。

应用场景

在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代

标记-压缩(或标记-整理、Mark - Compact)算法

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。

标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark - Compact)算法由此诞生。

执行过程

第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象。

第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。

之后,清理边界外所有的空间。

优点

消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。

消除了复制算法当中,内存减半的高额代价。

缺点

从效率上来说,标记-整理算法要低于复制算法。

移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。

移动过程中,需要全程暂停用户应用程序(STW)。

对比三种垃圾收集算法
Mark-SweepMark-CompactCopying
速度中等最慢最快
空间开销少(但会堆积碎片)少(不堆积碎片)通常需要存活对象的两倍空间(不堆积碎片)
对象地址是否改变
分代收集(Generational collecting)

不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

在Hotspot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。

年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
Mark阶段的开销与存活对象的数量成正比。(GC Root 引用链遍历)
sweep阶段的开销与所管理区域的大小成正相关。(空间遍历)
compact阶段的开销与存活对象的数据成正比。

以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial old回收器作为补偿措施:当内存回收不佳(碎片导致的concurrent Mode Failure时),将采用Serial old执行Full GC以达到对老年代内存的整理。

增量收集算法(Incremental collecting)

如果垃圾回收时间过长,应用程序会被挂起很久(STW),将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental collecting)算法的诞生。

基本思路

如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理成复制工作。

缺点

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而个是整个堆空间,从而减少一次Gc所产生的停顿。

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间(region)。

每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

垃圾回收相关概念

System.gc()

在默认情况下,通过system.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

然而System.gc()仅仅是提醒垃圾收集器进行垃圾回收,但无法保证对垃圾收集器的调用。(不保证,不确定马上执行GC)

JVM实现者可以通过system.gc ()调用来决定JVM的GC行为。在一些特殊情况下,如我们正在编写一个性能基准测试,我们可以在运行之间调用system.gc()。

内存溢出(OOM)

由于GC一直在发展,一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。

大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。

javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存

主要原因

Java虚拟机的堆内存设置不够。
比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms、-Xmx来调整。

代码中创建了大量大对象,并且长时间不能被垃圾收集器收集。

内存泄漏(Memory Leak)

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。

但实际情况很多时候一些不太好的代码实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”

尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常,导致程序崩溃。

内存泄漏具体场景

单例模式
单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的(即使是后面不用该引用对象,即该外部引用对象无用),则会导致内存泄漏的产生。

一些提供close的资源未关闭导致内存泄漏
数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的。

Stop The World

stop-the-world ,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。

可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。

STW事件和采用哪款垃圾回收器无关,所有的垃圾回收器都有这个事件。

哪怕是G1也不能完全避免stop-the-world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

开发中不要用System.gc();会导致stop-the-world的发生。

垃圾回收的并发与并行

并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
    如ParNew、Parallel scavenge、Parallel old;
  • 串行(Serial) :相较于并行的概念,单线程执行。如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

如下图,绿色为用户线程,红色为垃圾收集线程:
在这里插入图片描述

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。(用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上,如:CMS、G1)

在这里插入图片描述

安全点(safepoint)

程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint) ”。

safe Point的选择很重要,如果安全点太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断:(目前没有虚拟机采用了)
    首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
  • 主动式中断:
    设置一个中断标志,各个线程运行到safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

安全区域(safe Region)

安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把 safe Region看做是被扩展了的safepoint。

引用

强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“object obj=new object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。

虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

强引用(Strong Reference)

在Java程序中,最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。

当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。

强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。

相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成Java内存泄漏的主要原因之一。

强引用具备以下特点
  • 强引用可以直接访问目标对象。
  • 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
  • 强引用可能导致内存泄漏。

软引用(Soft Reference)

内存不足即回收。当内存足够时,不会回收软引用的可达对象。当内存不够时,会回收软引用的可达对象。

软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue) 。

类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

弱引用(Weak Reference)

发现即回收。

弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。

但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

虚引用(Phantom Reference)

对象回收跟踪。

一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。

它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是返回null。

虚引用必须和引用队列(ReferenceQueue)联合使用

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。

终结器引用(Final Reference)

它用以实现对象的finalize ()方法,也可以称为终结器引用。

无需手动编码,其内部配合引用队列使用。

在GC时,终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize ()方法,第二次GC时才能回收被引用对象。

垃圾回收器

垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。

由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。

从不同角度分析垃圾收集器,可以将GC分为不同的类型。

垃圾回收器分类 (理解即可)

按线程数分

按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。

串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。

并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了 stop-the-world 机制。

按工作模式分

按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。

并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。

独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

按碎片处理方式分

按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。

压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。

非压缩式的垃圾回收器不进行这步操作。

按工作的内存区间分

按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

评估垃圾回收器的性能指标

吞吐量:运行用户代码的时间占总运行时间(运行用户代码的时间+内存回收的时间)的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

内存占用:Java堆区所占的内存大小。

现在行业标准(原则):在最大吞吐量优先的情况下,降低停顿时间。

垃圾回收器发展史

1999年随JDK1.3.1一起来的是串行方式的Serial GC ,它是第一款GC。ParNew垃圾收集器是 Serial 收集器的多线程版本。

2002年2月26日,Parallel GC 和 Concurrent Mark Sweep GC 跟随JDK1.4.2一起发布。

Parallel GC在JDK6之后成为HotSpot默认GC。

2012年,在JDK1.7u4版本中,G1可用。

2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。

2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。

2018年9月,JDK11发布。引入Epsilon垃圾回收器,又被称为"No-Op (无操作)"回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)。

2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。

2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。

2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macOS和windows上的应用。

7款经典的垃圾收集器

串行回收器:Serial 、Serial old
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G1

垃圾收集器与分代之间的关系

在这里插入图片描述
新生代收集器:serial、ParNew、Parallel scavenge
老年代收集器:Serial old、Parallel old、CMS
整堆收集器:G1

垃圾回收器的组合关系

在这里插入图片描述
其中Serial old作为CMS出现"concurrent Mode Failure"失败的后备预案。

(红色虚线)由于维护和兼容性测试的成本,在JDK 8时将serial+CMS、ParNew+Serial old这两个组合声明为废弃(JEP 173),并在DK 9中完全取消了这些组合的支持(JEP214),即:移除。

(绿色虚线)JDK 14中:弃用Parallel Scavenge和Serial0ld GC组合(JEP366)。

(青色虚线)JDK 14中:删除CMS垃圾回收器(EP 363)。

Serial回收器:串行回收

Serial 收集器作为HotSpot中Client模式下的默认新生代垃圾收集器。

Serial 收集器采用复制算法、串行回收和"stop-the-world"机制的方式执行内存回收。

除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的serial old收集器。Serial old收集器同样也采用了串行回收和"stop the world"机制,只不过内存回收算法使用的是标记-压缩算法。

  • Serial old是运行在client模式下默认的老年代的垃圾回收器
  • Serial old在server模式下主要有两个用途:①与新生代的Parallel scavenge配合使用②作为老年代CMS收集器的后备垃圾收集方案

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个 CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The world)。

ParNew回收器:并行回收

ParNew收集器是Serial收集器的多线程版本。

Par是Parallel的缩写,New:只能处理的是新生代

ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。

ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。

ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器。

Parallel Scavenge回收器:吞吐量优先

HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stopthe world"机制。

那么Parallel收集器的出现是否多此一举?

  • 和ParNew收集器不同,Parallel scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput) ,它也被称为吞吐量优先的垃圾收集器。
  • 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。

Parallel 收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel old收集器,用来代替老年代的serial old收集器。

Parallel old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制。

在程序吞吐量优先的应用场景中,Parallel收集器和Parallel 0ld收集器的组合,在server模式下的内存回收性能很不错。

在Java8中,默认是此垃圾收集器。

CMS回收器:低延迟

CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。

  • 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

CMS的垃圾收集算法采用标记-清除算法,并且也会"stop-the-world"

工作原理

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段并发标记阶段重新标记阶段并发清除阶段

初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程并行。
在这里插入图片描述

CMS的有点

并发收集
低延迟

CMS的弊端

会产生内存碎片。

CMS收集器对CPU资源非常敏感。在并发阶段会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。

CMS收集器无法处理浮动垃圾。在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

G1回收器:区域分代

G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、survivor0区,survivor1区,老年代等。

G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

在JDK1.7版本正式启用,移除了 Experimental (实验性)的标识,是JDK 9以后的默认垃圾回收器,取代了CMS回收器以及Parallel + Parallel old组合。被oracle官方称为“全功能的垃圾收集器”。

优势

并行与并发
并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。

分代收集
从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。
将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
和之前的各类回收器不同,它同时兼顾年轻代和老年代的垃圾收集。

空间整合
G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。
Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。

可预测的停顿时间模型(即:软实时soft real-time)
这是G1 相对于CMS 的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。

G1跟踪各个 Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

不足

相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。

从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

适用场景
  • 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
  • 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案。
  • 用来替换掉JDK1.5中的CMS收集器。
  • HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
Region的介绍

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。

虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。

一个region有可能属于Eden,Survivor 或者 old/Tenured内存区域。G1 垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域(H)。主要用于存储大对象,如果对象超过1.5个region,就放到H。

G1垃圾回收过程

在这里插入图片描述

年轻代GC(Young GC)
应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程。 G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到survivor区间或者老年区间,也有可能是两个区间都会涉及。

年轻代垃圾回收只会回收Eden区和Survivor区。

老年代并发标记过程(concurrent Marking)
当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。

混合回收(Mixed GC)
标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间(整理)也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同。**G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。**同时,这个老年代Region是和年轻代一起被回收的。

Full GC
如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。

G1的Remember Set
G1回收器优化建议

避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小,因为固定年轻代的大小会覆盖暂停时间目标。

不要对垃圾回收时的暂停时间过于严苛,G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间。要求暂停时间过短,也会直接影响到吞吐量。

经典垃圾收集器总结
垃圾收集器分类作用位置使用算法特点适用场景
Serial串行运行新生代复制算法响应速度优先适用于单CPU环境下的client模式
ParNew并行运行新生代复制算法响应速度优先多CPU环境Server模式下与CMS配合使用
Parallel并行运行新生代复制算法吞吐量优先适用于后台运算而不需要太多交互的场景
Serial Old串行运行老年代标记-压缩算法响应速度优先适用于单CPU环境下的client模式
Parallel Old并行运行老年代标记-压缩算法吞吐量优先适用于后台运算而不需要太多交互的场景
CMS并发运行老年代标记-清除算法响应速度优先适用于互联网或B/S业务
G1并发、并行运行新生代、老年代复制算法、标记-压缩算法响应速度优先面向服务端应用
垃圾回收器选型

如果你想要最小化地使用内存和并行开销,请选Serial GC。
如果你想要最大化应用程序的吞吐量,请选Parallel GC。
如果你想要最小化GC的中断或停顿时间,请选CMS GC。

优先调整堆的大小让JVM自适应完成。

如果内存小于100M,使用串行收集器。

如果是单核、单机程序,并且没有停顿时间的要求,串行收集器。

如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择。

如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器。官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。

常用指令

javap

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值