JVM 详解(JVM组成部分、双亲委派机制、垃圾回收算法、回收器、回收类型、了解调优思路)

发布时间:2023年12月17日

目录

JVM 详解(JVM组成部分、双亲委派机制、垃圾回收算法、回收器、回收类型、了解调优思路)


1、概念:

什么是 JVM ?

JVM 就是 Java 虚拟机,是用来解析和运行Java程序的。

JVM 的性能调优是指通过优化程序的代码和环境,来提高程序运行效率、速度和稳定性的过程。


JVM 的作用?

为什么Java项目打包成一个Jar,就可以在windows、linux、MacOS 系统上运行,这就要归功于 JVM 。

Java代码在编译之后,并不是直接编译成我们操作系统可以识别的机器码,而是编译成只有 JVM 能够识别的字节码。
所以无论 Java 程序在哪个环境里面去运行,只要这个操作系统能装 JVM ,那么这个 Java 程序就能够运行。
JVM 相当于在做翻译工作,动态的把我们的Java代码翻译成操作系统能够识别的机器码。这样我们的Java代码就能实现一次编译,处处运行。
(通过JVM 使 Java 语言在不同平台上运行时不需要重新编译)

比如 JDK 和 JRE 就包含了 JVM.
在这里插入图片描述


2、JVM 的主要组成部分?

类加载器、运行时内存区(数据区)、执行引擎 三部分组成。

运行时内存区:堆、栈、方法区 等。

在这里插入图片描述


类加载器(Class Loader):


简单说下作用:

在这里插入图片描述
Java虚拟机Java源码 编译为 字节码 之后,虚拟机便可以将字节码读取进 内存,从而进行解析、运行等整个过程,这个过程我们叫:Java虚拟机的类加载机制。

JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

在这里插入图片描述

简单来说:就是负责加载 Java 类文件到 JVM 中,并将其转换为在 JVM 中使用的可执行代码。


运行时内存区(Runtime Data Area):

官方叫:运行时数据区

是 JVM 的内存空间,用于存储程序运行时所需的数据。
JVM在运行时划分了不同的内存区域来存储数据: 堆、栈、方法区

「运行时数据区」也可以叫做是「虚拟机内存结构」我们通常叫的比较多的还是JVM虚拟机内存结构。它指运行时会把它管理的内存划分成若干个不同的数据区域 ,简单的说就是不同的数据放在不同的地方。
共分为五个部分:堆、方法区、虚拟机栈、程序计数器、本地方法栈。

线程共享: 堆、方法区
线程私有: 虚拟机栈、程序计数器、本地方法栈
在这里插入图片描述


执行引擎:

是 JVM 的核心组件,负责解释和执行 Java 字节码指令。

我们最终代码是要运行的,这部分工作就是由执行引擎来完成。
它会把分配给 运行时数据区 的字节码交给执行引擎来执行。执行引擎则会读取字节码并一段一段的执行它。

Java是一门半解释半编译型语言,所以执行引擎又分为了 解释器JIT编译器

解释器: 就是当Java虚拟机启动时根据预定义的规范把字节码翻译成对应的机器码逐行去解释执行。
JIT编译器 是虚拟机将源代码直接编译成机器码。

在这里插入图片描述


内存区的堆、栈、方法区:


栈 :

存放方法。每个线程在运行时都会创建一个栈,用于存储方法调用时的局部变量、方法参数、方法返回值等。栈是线程私有的内存区域


堆:

用于存放对象实例和数组


方法区 :

存放类的结构信息(类的字段、方法、构造函数等),
运行时常量池(各种常量:字符串常量、数值常量、类和接口的符号引用等),
方法区是各个线程共享的内存区域。

这个图就是运行时内存区的大致:
在这里插入图片描述
详细:
在这里插入图片描述

运行时内存区,也可以叫运行时数据区
在《深入理解Java虚拟机》第三版第43页的图也是如此划分,直接引用其他大佬画好的图片:
在这里插入图片描述


3、JVM 类加载器各阶段作用?


类加载器的作用:

它就是读取字节码转换成java.lang.Class类的一个实例,然后通过newInstance()方法就可以创建类的实例。

说起来一句话非常简单,实际的情况可能更加复杂,比如Java字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

在这里插入图片描述
在Java语言里,类型的加载、连接、初始化都是在程序运行期间完成的,这种策略让类加载时稍微增加一些性能开销,但也为Java应用提供了高度的灵活性。

Java天生可以动态扩展的语言特性就是基于运行期动态加载和动态连接这个特点实现的。

类的生命周期:

从类加载到虚拟机内存开始,到卸载为止,生命周期一共分为以下七步,如图:
其中固定的顺序:加载–验证–准备–初始化–卸载

其中 验证、准备 和 解析 三个部分统称为 连接

【解析】也可以在【初始化】之后再进行解析,这是为了支持Java语言在运行时的一个动态绑定的特性。
在这里插入图片描述


1、加载阶段:

简单来说:
从不同的数据源( class类文件、jar包、网络)中获取到的字节码,获取到时还是一个二进制的一个字节流,然后把它加载到内存里面,转化成一个class类对象。加载该类后生成的代表该类的对象则存在 Java堆 里面。
方法区则存该类的结构信息和运行时常量池。


详细分析:

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:

1、通过一个类的全限定名来获取定义此类的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且 在 Java堆中也创建一个java.lang.Class类的对象 ,这样便可以通过该对象访问方法区中的这些数据。

全限定类名(Fully Qualified Class Name)指的是一个类的完整名称,包括包名、类名和其内部类的名称。
全限定类名可以用于唯一地标识一个类,包括标准类、抽象类、接口和枚举类型。

2、验证阶段

简单来说:验证阶段就是对我们传入的一个二进制字节流去进行校验,只有符合 JVM 字节码规范的,才能被 JVM 正确执行。这个阶段就是为了保证 JVM 安全的一个重要凭证


验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

在这里插入图片描述

在计算机领域中,“魔数”(Magic Number)是指一种特定的固定字节序列,用于识别文件格式或数据类型。它通常位于文件的开头,作为文件的标识符。

对于字节码文件(.class 文件)来说,魔数是指文件开头的四个字节,以十六进制表示为 “0xCAFEBABE”。这个特定的字节序列被设计为Java虚拟机用来识别和验证字节码文件的标识


3、准备阶段:

简单来说:
JVM在这个准备阶段,对类的变量去进行一个分配内存和初始化。

注意:准备阶段的这个初始化是初始化默认值,比如我代码中定义一个 int a = 10 ,此时这个 a 是等于默认值 0 。因为 int 类型的默认值是0 。

真正把 10 赋值给 a 变量的,是在【初始化】阶段。


详细解释:

为类的静态变量分配内存,并将其初始化为默认值。

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。


4、解析阶段:

简单来说:就是将常量池中的符号引用转换为直接引用。

符号引用:以一组符号,比如任何形式的字面量(常量),来描述引用的目标,这些字面量就叫做符号引用。

直接引用:通过对这个符号引用去进行一个解析,然后找到引用的一个实际内存地址在哪里并做一个关联,这个就叫做直接引用。

在这里插入图片描述


5、初始化阶段

简单来说:就是一个正常赋值的阶段,也可以理解是去执行类构造器方法的一个过程。

区别理解:
准备阶段:给变量赋予默认值
初始化阶段:给变量赋予真正的值。

详细解释:
在这里插入图片描述


6、使用阶段:

简单来说:当整个界面完成初始化阶段后,JVM 就开始从入口方法执行代码,比如从main方法开始执行我们程序的代码。


7、卸载阶段:

简单来说:当代码执行完成之后, JVM 就要开始销毁前面所创建的对象。

JVM的卸载阶段是一个高度优化的过程,并不是每个类或对象都会被立即卸载。JVM会根据一系列的条件和策略进行判定和调度,以提高执行效率和资源利用率。

需要明确的是,JVM的卸载阶段主要针对普通的应用类。而核心的系统类(如Java标准库中的类)通常会被JVM认为是永远可达的,不会被卸载。

卸载阶段主要是为了释放应用程序中自定义类所占用的内存和资源。


4、JVM 的类加载器有哪些?

从Java程序的角度看,有这三种类加载器:
启动类加载器、扩展类加载器、应用程序类加载器。

而从虚拟机的角度看,只有两种类加载器:
C++ 语言写的 启动类加载器,属于虚拟机自身的一部分。
Java 语言写的 扩展类加载器 和 应用程序类加载器,是独立于 JVM 的外部的。

在这里插入图片描述

--------------------------------------------------------------------------


启动类加载器:
这个类加载器是不能被Java程序直接引用的,就算是通过代码去获取到,也是一个null值。


扩展类加载器:
负责加载 lib包、ext扩展包的文件,可以被开发者直接使用的。


应用程序类加载器:
也被称为系统类加载器。
负责加载的是程序的一些类路径,比如第三方类库、ClassPath。
如果应用程序没有自定义自己的类加载器,那么就默认使用这个应用程序类加载器。


这三个类加载器是相互配合、互相工作的。如上图,各个类加载器是分层的,这些分层也被称为类加载器的双亲委派机制。

JVM设计者把类加载阶段中的“通过’类全名’来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。


类与类加载器的关系:

对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。

--------------------------------------------------------------------------


什么双亲委派机制

简单来说:双亲委派机制是 Java 虚拟机的一种类加载机制,它是实现 Java 安全沙箱和避免类重复加载的重要手段。

1、要求最顶层是启动类加载器,其余的类加载器都应该有自己的父类加载器。
比如扩展类加载器的父类就是启动类加载器。
应用程序类加载器的父类就是扩展类加载器

2、子类加载器和父类加载器并不是一个继承的关系,而是通过组合的关系,来复用父类加载器里面的代码。

安全沙箱(Security Sandbox)是一种安全机制,用于限制程序在执行过程中的操作权限,防止恶意代码对计算机系统造成破坏或滥用系统资源


双亲委派机制的工作过程

这个图也是双亲委派机制的流程图:
在这里插入图片描述

比如应用程序类加载器,收到了一个类加载的请求,它首先并不会去尝试自己加载这个类,而是把这个请求委派给父类加载器去完成,每一层都是如此。


步骤理解:

1、比如应用程序类加载器接收到一个类加载的请求,它不会马上去进行类加载操作,而是委派给自己的父类加载器->扩展类加载器。

2、扩展类加载器就会检查自己是否已经加载过这个类,如果已经加载,则直接返回对应的 Class 对象。如果自身没有加载过该类,则将加载请求再继续向上委派给父类加载器->启动类加载器。
(注意点:如果扩展类加载器没有加载过这个类,但是它自身具备加载这个类的能力,根据双亲委派模型的原则,扩展类加载器会继续委派加载请求给父类加载器。)

3、如此一层一层的检查和向上委派,直到委派到最顶层的启动类加载器后,
如果顶层的启动类加载器能够来加载这个类,那么就由启动类加载器来加载这个类。
如果这个启动类加载器不能加载这个类,那么它再把这个类加载的请求,往下去分派给它的子类加载器(扩展类加载器),如果扩展类加载器也加载不了,那么再往下分派给 应用程序类加载器。

只有当所有的父类加载器都无法加载时,才会由当前类加载器自行尝试加载。
如果加载失败,那就抛异常。

一般不会加载失败。
类加载失败的情况一般是:
在类路径中找不到指定的类文件、类文件格式错误、该类依赖的类不存在等情况



双亲委派机制的好处:

通过这种模型来组织类加载器之间的关系,
好处是:
比如有一个Object类,无论是哪个类加载器去加载这个类,最终都是由启动类加载器来加载的,因此这个 Object 类在我们程序里面, 不论什么样的环境,都是使用同一个 Object 类。

如果我们不使用这个双亲委派机制:
比如我想尝试自己定义一个Object类,然后存放在Classpath 里面, 那么系统就会出现多个 Object 类,程序就会混乱。

如果我们使用这个双亲委派机制:
如果我们定义了一个 rt.jar 里面已经有了的同名类,我们会发现,JVM可以进行正常编译,但是这个类永远无法被加载运行。
比如我们自己定义一个Object类,虽然这个类能被JVM编译,但是不会被加载和运行。


rt.jar 是 JDK(Java Development Kit) 中以前的一个核心库文件,它包含了大量 JDK 中的基础类库和 API,如 Java 标准库、Java 集合框架、Java 网络编程相关库等。rt 代表 “run-time”,也就是运行时库

在 JDK 9 及以后的版本中,rt.jar 被拆分成了多个模块(Module)和 JAR 包,并不再作为独立的库文件存在。这是为了更好地支持模块化开发,提高代码的可维护性和扩展性


总结:

这是jdk1.8的理解:

双亲委派机制就是 当类加载器接收到一个类加载的请求,这个类加载器不会马上就去进行加载操作,而是向上委托给自己的父类(也是类加载器),询问父类加载器是否已经加载这个类,如果加载过,则返回该类的Class对象,如果没加载过,但是本身具备加载该类的条件,该类加载器还是会继续向自己的父类去委托,直到委托给最顶层的启动类加载器。

最顶层的启动类加载器已经没有父类了,就只能自己来加载这个类,如果启动类加载器发现自己加载不了,它就会向下分派给自己的子类(也是类加载器)去加载。

用这个机制的好处就是如果出现恶意的和基础类库同名的类,这个类虽然会被JVM编译,但是不会被加载运行。


jdk1.9 以后:
双亲委派机制发生了变化,简单说:
就是没有了扩展类加载器,变成了平台类加载器,比如应用程序类加载器把类加载的请求委托给 平台类加载器,如果这个平台类加载器能找到该类,就直接进行加载的操作,不会跟jdk1.8一样委派给启动类加载器。

在这里插入图片描述


问题:
jdk1.9之后,如果应用程序类把一个类加载的请求发送给平台类加载器,平台类加载器没有加载过该类,但是自身具备加载该类的条件,那么它会自己加载还是继续委托给启动类加载器?

解释:

根据 JDK 9 之后的模块化系统,如果应用程序类把一个类加载的请求发送给平台类加载器,并且平台类加载器具备加载该类的条件,它会根据类所属模块的定义来决定加载行为。对于平台模块的类,平台类加载器会自己加载;对于应用程序模块的类,平台类加载器会继续委托给启动类加载器。

JDK 9 之后的类加载机制更加灵活和复杂,涉及到了模块化系统的概念。因此,具体的加载行为还取决于模块定义、模块路径和类路径的配置等因素


其他说法:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述



5、JVM 的内存结构(运行时内存区)

官方叫运行时数据区
在这里插入图片描述


线程共享数据:

堆、方法区、运行时常量池、直接内存


Java堆:

简单来说:堆就是从 JVM 划分出来的一块区域,是虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此内存区域的唯一目的就是用来用来存放实例对象,几乎所有的实例都会在这个 Java堆 进行内存分配。
还有一些小的对象,会在栈上分配。

堆是比较重要的部分,要了解好堆的区域划分情况:
堆的划分:
在这里插入图片描述

详细解释:

1、Java堆根据对象的存活时间的不同,还会再进行划分—> 年轻代 和 老年代 。

2、年轻代 划分了 Eden区 和 幸存者区,当有对象要进行内存分配的时候,永远优先分配在 Eden区 的,等到 Eden区 内存不够的时候,虚拟机就会去启动 GC(垃圾回收机制) ,此时 Eden区 没有被引用的对象,那这些对象的内存就会被回收掉。而一些存活时间比较长的对象,就会进入到老年代里面。

3、因为虚拟机里面的对象有的存活时间短,有的存活时间长,如果我们将其混在一起,那么因为存活时间短的对象有很多,那么势必导致较为频繁的垃圾回收。而垃圾回收时不得不对所有内存都进行扫描,但其实有一部分对象,它们存活时间很长,对他们进行扫描完全是浪费时间。因此为了提高垃圾回收效率,分区就理所当然了,所以对堆空间进行区域划分,能提高GC的效率

5、在 Java 虚拟机里面,默认年轻代的配置是 8 : 1 : 1 的空间分配,Eden区的比例是 8 ,幸存者区的比例是 1 。
根据统计, 80% 的对象的存活时间比较短,所以把Eden区设置为年轻代的 80% ,这样能减少内存空间的浪费,提高内存空间的使用率。


方法区:

一个java文件通过类加载器加载到内存里面,然后这个类的结构信息就会存在这个方法区里面。

是存放的是 Java类字节码数据的一块区域,存每个类的结构信息,包括字段、方法数据、构造方法等等。
方法区在 JDK1.7 称为 永久代,在 JDK1.8 称为 元空间。


线程私有数据:

程序计数器、虚拟机栈、本地方法栈、线程分配缓冲区


程序计数器:

简单来说:JVM的程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

这个字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
(个人理解就是,因为 Java虚拟机 的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的, 当这条线程执行后要切换下一条线程时,具体切换哪条线程,要从哪个位置开始执行,就得通过程序计数器来实现)

每条线程都有一个独立的程序计数器,作用是保证线程在切换后能恢复到正确的执行位置。

各线程之间的程序计数器互不影响、独立存储。


虚拟机栈

虚拟机栈 和 线程 是同一时间创建出来的,生命周期 和 线程 相同。

虚拟机栈描述的是 Java方法在执行时的线程内存模型:
每个方法被执行的时候,Java虚拟机 就会同步创建一个栈帧,这个栈帧用来存储 Java方法 执行时的局部变量、操作数栈、动态连接、方法出口等信息

每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。


本地方法栈

本地方法栈和虚拟机栈发挥的作用是非常相似的,都是对方法执行期间的相关数据的存储。

区别:
虚拟机栈 为虚拟机执行Java方法(也就是字节码)进行服务;
本地方法栈 是为虚拟机使用到本地方法(Native)进行服务。

在 Java 中,本地方法声明通常使用 “native” 关键字:
public native void nativeMethod();


总结:

一个 Java文件,在 Java虚拟机 里面,通过 类加载器 加载到内存里面时,这个类的结构信息就会存在这个 方法区 里面,如果创建对象,那么这个对象的数据就放在 Java堆 里面,如果调用方法,那么就会用到 程序计数器、虚拟机栈或者本地方法栈


6、JVM 有哪些垃圾回收算法(Garbage Collection)

因为内存是有限的,需要有一个机制来回收已经不再使用的内存,从而实现内存的循环利用,程序才能正常运转。

垃圾回收管理的就是堆内存

在这里插入图片描述


内存管理:
Java的内存管理很大程度上就是:堆中对象的管理,其中包括对象空间的分配和释放。
对象空间的分配: 使用new关键字创建对象,就是一种空间分配
对象空间的释放: 将对象赋值为null即可,然后垃圾回收机制会通过底层代码发现这个对象为null,没有被其他对象引用,就把它回收了。


1、什么样的对象是垃圾?

一个对象,如果没有任何变量去引用,那么这个对象就是垃圾了,需要被回收,回收其占用的内存空间。


2、如何判断是不是垃圾的算法


1、引用计数法(Python 或 一些脚本 会用这个)
简单来说:

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就 +1;当引用失效时,计数器值就 -1 ;任何时刻计数器值为 0 的对象就是不可能再被使用的,需要回收的。

出现的问题:

因为引用计数法,是对没有被引用的对象进行回收,如果有两个对象互相引用着,但除此之外再无任何引用。实际上这两个对象已经不可能再被访问,但是他们又因为互相引用,导致它们的引用计数器都不为0,所以引用计数法也就无法回收它们,但是了内存空间的浪费。
在这里插入图片描述

代码示例:

在这里插入图片描述


2、引用可达法(Java项目用这个)

而现今的 Java 虚拟机判断垃圾对象使用的是:GC Root Tracing 算法。

基本思路:通过一系列称为 " GC Roots " 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链” ,如果某个对象到 GC Roots 间没有任何引用链相连,证明这个对象是不可能再被使用的,就可以回收了。

用图来表示,简单的说:从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾。

如图: Object5 虽然跟 Object6 有引用关系,但是和 GC Roots 没有引用关系,所以判定为可回收对象。
在这里插入图片描述


3、如何进行垃圾回收

垃圾回收算法简单地说有三种算法:标记清除算法、复制算法、标记压缩算法


标记清除算法:

从名字可以看到其分为两个阶段:标记阶段和清除阶段。一种可行的实现方式是,在标记阶段,标记所有由 GC Root 触发的可达对象。此时,所有未被标记的对象就是垃圾对象。之后在清除阶段,清除所有未被标记的对象。标记清除算法最大的问题就是空间碎片问题。如果空间碎片过多,则会导致内存空间的不连续。虽说大对象也可以分配在不连续的空间中,但是效率要低于连续的内存空间。


复制算法:

复制算法的核心思想是将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象,之后交换两个内存块的角色,完成垃圾回收。该算法的缺点是要将内存空间折半,极大地浪费了内存空间。


标记压缩算法:

标记压缩算法可以说是标记清除算法的优化版,其同样需要经历两个阶段,分别是:标记结算、压缩阶段。在标记阶段,从 GC Root 引用集合触发去标记所有对象。在压缩阶段,其则是将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间。


优缺点:

标记清除算法 虽然会产生内存碎片,但是不需要移动太多对象,比较适合在存活对象比较多的情况。


复制算法 虽然需要将内存空间折半,并且需要移动存活对象,但是其清理后不会有空间碎片,比较适合存活对象比较少的情况。


标记压缩算法 则是标记清除算法的优化版,减少了空间碎片。


分代思想:

在这里插入图片描述


分区思想:

在这里插入图片描述


7、 JVM 有哪些垃圾回收器

总的来说,Java 虚拟机的垃圾回收器可以分为四大类别:串行回收器、并行回收器、CMS 回收器、G1 回收器。

在这里插入图片描述


串行回收器

串行回收器是指使用单线程进行垃圾回收的回收器。因为每次回收时只有一个线程,因此串行回收器在并发能力较弱的计算机上,其专注性和独占性的特点往往能让其有更好的性能表现。

串行回收器可以在新生代和老年代使用,根据作用于不同的堆空间,分为新生代串行回收器和老年代串行回收器。


新生代串行回收器

串行收集器是所有垃圾回收器中最古老的一种,也是 JDK 中最基本的垃圾回收器之一。
在新生代串行回收器中使用的是 复制算法。在串行回收器进行垃圾回收时,会触发 Stop-The-World 现象,即其他线程都需要暂停,等待垃圾回收完成。因此在某些情况下,其会造成较为糟糕的用户体验。
使用 -XX:+UseSerialGC 参数可以指定使用新生代串行收集器和老年代串行收集器。当虚拟机在 Client 模式下运行时,其默认使用该垃圾收集器。


老年代串行回收器

在老年代串行回收器中使用的是标记压缩算法。其与新生代串行收集器一样,只能串行、独占式地进行垃圾回收,因此也经常会有较长时间的 Stop-The-World 发生。
但老年代串行回收器的好处之一,就是其可以与多种新生代回收器配合使用。若要启用老年代串行回收器,可以尝试以下参数:

-XX:UseSerialGC:新生代、老年代都使用串行回收器。
-XX:UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器。
-XX:UseParallelGC:新生代使用 ParallelGC 回收器,老年代使用串行回收器。


并行回收器

并行回收器在串行回收器的基础上做了改进,其使用多线程进行垃圾回收。对于并行能力强的机器,可以有效缩短垃圾回收所使用的时间。
根据作用内存区域的不同,并行回收器也有三个不同的回收器:新生代 ParNew 回收器、新生代 ParallelGC 回收器、老年代 ParallelGC 回收器


新生代 ParNew 回收器

在这里插入图片描述


新生代 ParallelGC 回收器

在这里插入图片描述


老年代 ParallelGC 回收器

在这里插入图片描述


CMS 回收器

与 ParallelGC 和 ParallelOldGC 不同,CMS 回收器主要关注系统停顿时间。CMS 回收器全称为 Concurrent Mark Sweep,意为标记清除算法,其是一个使用多线程并行回收的垃圾回收器。


工作步骤:

CMS 的主要工作步骤有:初始标记、并发标记、预清理、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而其他阶段则可以和用户线程一起执行。
在整个 CMS 回收过程中,默认情况下会有预清理的操作,我们可以关闭开关 -XX:-CMSPrecleaningEnabled 不进行预清理。因为重新标记是独占 CPU 的,因此如果新生代 GC 发生之后,立刻出发一次新生代 GC,那么停顿时间就会很长。为了避免这种情况,预处理时会刻意等待一次新生代 GC 的发生,之后在进行预处理。


主要参数:

在这里插入图片描述


G1 回收器

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


工作步骤:

在这里插入图片描述


相关参数:

在这里插入图片描述


8、JVM 垃圾回收的几种类型

垃圾回收的术语,例如:Minor GC、Major GC、Young GC、Old GC、Full GC、Stop-The-World 等。


Minor GC(年轻代空间回收内存)

从年轻代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。

1、当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以 Eden 区越小,越频繁执行 Minor GC。

2、当年轻代中的 Eden 区分配满的时候,年轻代中的部分对象会晋升到老年代,所以 Minor GC 后老年代的占用量通常会有所升高。

3、质疑常规的认知,所有的 Minor GC 都会触发 Stop-The-World,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的,因为大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果情况相反,即 Eden 区大部分新生对象不符合 GC 条件(即他们不被垃圾回收器收集),那么 Minor GC 执行时暂停的时间将会长很多(因为他们要JVM要将他们复制到 Survivor 区或老年代)。


Major GC(老年代空间回收内存)

从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。
许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。
Minor GC 作用于年轻代,Major GC 作用于老年代。 分配对象内存时发现内存不够,触发 Minor GC。Minor GC 会将对象移到老年代中,如果此时老年代空间不够,那么触发 Major GC。因此才会说,许多 Major GC 是由 Minor GC 引起的。


Full GC(清理整个堆空间)

Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(如果有的话)。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。
当准备要触发一次 Minor GC 时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC 而是转为触发 Full GC。因为JVM此时认为:之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那么我就直接帮你把事情给做了吧,直接来一次 Full GC,整理一下老年代和年轻代的空间。
另外,即在永久代分配空间但已经没有足够空间时,也会触发 Full GC。


Stop-The-World(全世界暂停)

Stop-The-World,中文一般翻译为全世界暂停,是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。

在 Stop-The-World 这段时间里,所有非垃圾回收线程都无法工作,都暂停下来。只有等到垃圾回收线程工作完成才可以继续工作。可以看出,Stop-The-World 时间的长短将关系到应用程序的响应时间,因此在 GC 过程中,Stop-The-World 的时间是一个非常重要的指标。


9、JVM 调优思路


为什么要调优

1、防止出现OOM
2、解决OOM
3、减少Full GC出现的频率

JVM调优是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,因此,在进行JVM调优时,我们要遵循一些原则:
在这里插入图片描述


什么是 OOM ?(内存溢出)

OOM(Out of Memory)是指程序在运行过程中无法分配到足够的内存空间而导致的错误。当应用程序需要更多的内存来创建对象或执行操作时,JVM会尝试分配内存,但如果可用的内存已经耗尽,将会发生OOM错误


JVM调优的一般步骤为:

第1步:分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
第2步:确定JVM调优量化目标;
第3步:确定JVM调优参数(根据历史JVM参数来调整);
第4步:调优一台服务器,对比观察调优前后的差异;
第5步:不断的分析和调整,直到找到合适的JVM参数配置;
第6步:找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。


分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点

在这里插入图片描述


GC日志分析

在这里插入图片描述


Java程序

在这里插入图片描述


Tomcat

在这里插入图片描述


SpringBoot

在这里插入图片描述


分析GC日志可视化工具

在这里插入图片描述


以下情况不需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
如果满足下面的指标,则一般不需要进行GC:
1、 Minor GC执行时间不到50ms;
2、 Minor GC执行不频繁,约10秒一次;
3、 Full GC执行时间不到1s;
4、Full GC执行频率不算频繁,不低于10分钟1次;


系统崩溃前的一些现象

在这里插入图片描述


JVM 的一些调优参数:

在这里插入图片描述
在这里插入图片描述






本文章关于JVM的知识点截图,借鉴于图灵课堂。
文章来源:https://blog.csdn.net/weixin_44411039/article/details/134908341
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。