一、基础概念
1.1、JVM介绍
JVM
:Java Virtual Machine
,Java
虚拟机- 位置:
JVM
是运行在操作系统之上的,它与硬件没有直接的交互。
- Java是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要JVM进行一番转换。有了VM这个抽象层之后,Java就可以实现跨平台了。JVM只需要保证能够正确执行.class文件,就可以运行在诸如Linux、Windows、MacOs等平台上了。而Java跨平台的意义在于一次编译,处处运行,能够做到这一点VM功不可没。
1.2、主流的虚拟机
JCP
组织(Java Community Process
开放的国际组织):Hotspot
虚拟机(Open JDK
版),sun2006年开源。Oracle
:Hotspot
虚拟机(Oracle JDK
版),闭源,允许个人便用,商用收费。BEA
:JRockit
虚拟机。IBM
:J9虚拟机。- 阿里巴巴:
Dragonwell JDK
(龙井虚拟机),电商物流金融等领域,高性能要求。
二、内存结构
2.1、JVM内存结构
- 整体结构
- 加载器子系统
- 运行时数据区(左边亮的可以被GC)
- 执行引擎
- 本地方法接口(其他语言)
- 运行时数据区
- 整体结构
2.1.1、运行时数据区
1.堆
- 堆是 JVM 中最大的一块内存区域,用于存储对象实例和数组。所有线程共享堆内存。
- 新生代(
Young Generation
):存放新创建的对象,新生代使用复制算法进行垃圾回收。Eden
区:对象最初分配的区域。Survivor
区(From
和To
):存放经过一次垃圾回收后存活的对象。
- 老年代(
Old Generation
):存放长期存活的对象,当对象在新生代经过多次垃圾回收后仍然存活,会被晋升到老年代,使用标记-清除算法或标记-整理算法进行垃圾回收。
- 新生代(
- 特点:
- 堆内存的大小可以通过 JVM 参数调整:
- -Xms:设置堆的初始大小。
- -Xmx:设置堆的最大大小。
- 堆是垃圾回收的主要区域。
- 堆内存的大小可以通过 JVM 参数调整:
2.方法区(Method Area
)
-
方法区用于存储类信息、运行时常量池、静态变量、JIT代码缓存(即时编译器编译后的代码)、域信息、方法信息等。所有线程共享方法区。
-
实现:
- 在 JDK 8 之前,方法区的实现为永久代(
PermGen
)。 - 在 JDK 8 及之后,方法区的实现为元空间(
Metaspace
),使用本地内存。
- 在 JDK 8 之前,方法区的实现为永久代(
-
特点:
- 方法区的大小可以通过 JVM 参数调整:
-XX:MetaspaceSize
:设置元空间的初始大小。-XX:MaxMetaspaceSize
:设置元空间的最大大小。
- 方法区的垃圾回收主要针对常量池和类卸载。
- 方法区的大小可以通过 JVM 参数调整:
-
去永久代的原因有:
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
3.栈(Stack
)
- 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,每个线程都有自己的栈,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,是线程私有的。线程上正在执行的每个方法都各自对应一个栈帧(
Stack Frame
)。栈用于存储局部变量、方法调用、操作数栈等。 JVM
对Java
栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出或者后进先出原则。- 个线程中只能由一个正在执行的方法(当前方法),因此对应只会有一个活动的”当前栈帧”。
- 栈帧内存分为以下几个部分:
- 局部变量表:存储方法的局部变量。
- 操作数栈:用于存储操作数和中间结果。
- 动态链接:指向运行时常量池的方法引用。
- 方法返回地址:存储方法的返回地址。
- 一些附加信息。
- 特点:
- 栈内存的大小可以通过 JVM 参数调整:
-Xss
:设置每个线程的栈大小。
- 栈内存的分配是连续的,超出栈大小会抛出
StackOverflowError
。
- 栈内存的大小可以通过 JVM 参数调整:
4.本地方法栈(Native Method Stack
)
- 本地方法栈用于支持
本地方法(Native Method)
的执行。本地方法栈与虚拟机栈的区别是,虚拟机栈执行的是Java
方法,本地方法栈执行的是本地方法(Native Method
),其他基本上一致,在 HotSpot 中直接把本地方法栈和虚拟机栈合二为一。 - 特点:
- 本地方法栈与栈类似,但专门用于本地方法。
- 本地方法栈的大小可以通过
JVM
参数调整。
5.程序计数器(Program Counter Register
)
- 程序计数器用于记录当前线程执行的字节码指令地址,每个线程有独立的程序计数器。
- 特点:
- 程序计数器是唯一 一个不会抛出
OutOfMemoryError
的区域。 - 如果执行的是本地方法,程序计数器的值为
undefined
。
- 程序计数器是唯一 一个不会抛出
2.1.2、本地方法接口Java Native Interface
(JNI
)
- 本地接口的作用是融合不同的编程语言为
Java
所用,于是就在内存中专门开辟了一块区域处理标记为native
的代码,它的具体做法是Native Method Stack
中登记native
方法,在Execution Engine
执行时加加载native libraies
。
2.1.3、执行引擎
Execution Engine
执行引擎负责解释命令(将字节码指令解释编译为机器码指令),提交提作系统执行。JVM
执行引擎通常由两个主要组成部分构成:解释器和即时编译器(Just-In-Time Compiler
,JIT Compiler
)- 解释器(
Just-In-Time Compiler
):当Java
字节码被加载到内存中时,解释器逐条解析和执行字节码指令,解释器逐条执行字节码,将每条指令转换为对应平台上的本地机器指令。由于解释器逐条解析执行,因此执行速度相对较慢。但解释器具有优点,即可立即执行字节码,无需等待编译过程。 - 即时编译器(
JIT Compiler
):为了提高执行速度,JVM
还使用即时编译器。即时编译器将字节码动态地编译为本地机器码,以便直接在底层硬件上执行。即时编译器根据运行时的性能数据和优化技术,对经常执行的热点代码进行优化,从而提高程序的性能。即时编译器可以将经过优化的代码缓存起来,以便下次再次执行时直接使用。
- 解释器(
JVM
执行引擎还包括其他一些重要的组件,如即时编译器后端、垃圾回收器、线程管理器等,这些组件共同协作,使得Java
程序能够在不同的操作系统和硬件平台上运行,并且具备良好的性能。
2.2、栈
2.2.1、栈帧结构
1.局部变量表(Local Variable Table
)
- 也叫本地变量表,用于存储方法的局部变量,包括方法参数和方法内部定义的变量。局部变量表以变量槽(
Slot
)为单位,每个Slot
可以存储一个基本数据类型(如int
、float
、boolean
等)或对象引用(reference
)。对于long
和double
类型的数据,需要占用两个连续的Slot
。局部变量表的容量在编译时确定,并存储在方法的字节码中。 javap -v 类名.class
可查看局部变量。或者在idea中装jclasslib
插件- 举例
public void example(int a, int b) {
int c = a + b;
}
//局部变量表:a(Slot 0)、b(Slot 1)、c(Slot 2)。
2.操作数栈(Operand Stack
)
- 用于存储方法执行过程中的操作数和中间结果。操作数栈是一个后进先出(
LIFO
)的栈结构。字节码指令从操作数栈中取出操作数,执行计算后将结果压入栈中。操作数栈的深度在编译时确定,并存储在方法的字节码中。 - 示例
public int add(int a, int b) {
return a + b;
}
字节码指令:
iload_1 // 将局部变量表 Slot 1 的值(a)压入操作数栈
iload_2 // 将局部变量表 Slot 2 的值(b)压入操作数栈
iadd // 弹出栈顶的两个值相加,将结果压入栈中
ireturn // 返回栈顶的值
- 图解
3.动态链接(Dynamic Linking
)
- 指向运行时常量池(
Runtime Constant Pool
)中该栈帧所属方法的引用(可以知道当前帧执行的是哪个方法),用于支持方法调用过程中的动态链接。 - 动态链接将符号引用(
Symbolic Reference
)转换为直接引用(Direct Reference
)。支持多态性(Polymorphism
),即在运行时确定具体调用的方法。- 示例
public class Animal {
public void sound() {
System.out.println("Animal sound");
}
}
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog bark");
}
}
Animal animal = new Dog();
animal.sound(); // 动态链接确定调用 Dog 类的 sound 方法
- 图解
4.方法返回地址(Return Address
)
- 存储方法执行完成后需要返回的地址,以便程序继续执行。对于正常返回(通过
return
指令),返回地址是调用者的下一条指令地址。对于异常返回(通过抛出异常),返回地址由异常处理器表(Exception Table
)确定。- 示例
public int add(int a, int b) {
return a + b;
}
public void main() {
int result = add(1, 2); // 调用 add 方法
System.out.println(result); // add 方法返回后继续执行
}
5.附加信息(Additional Information
)
- 存储一些与实现相关的附加信息,例如调试信息、性能监控数据、虚拟机版本信息,栈帧的高度等。这部分内容不是
JVM
规范强制要求的,具体实现可能有所不同。例如,HotSpot
虚拟机可能会在这里存储一些与即时编译(JIT
)相关的信息。
2.2.2、生命周期
- 创建:当一个方法被调用时,
JVM
会为其创建一个栈帧,并将其压入当前线程的Java
虚拟机栈。 - 执行:方法执行过程中,局部变量表和操作数栈会被频繁使用。
- 销毁:方法执行结束后(正常返回或抛出异常),栈帧会被弹出,其占用的内存被释放。
2.2.3、栈溢出
- 常见问题栈溢出:
Exception in thread"main” java.lang.StackOverflowError
通常出现在递归调用时。 - 问题辨析:
- 垃圾回收不涉及栈,因为栈内存在方法调用结束后都会自动弹出栈。
- 当方法内局部变量没有逃离方法的作用范围时线程安全,因为一个线程对应一个栈,每调用一个方法就会新产生一个栈桢,都是线程私有的局部变量,当变量是
static
时则不安全,因为是线程共享的。
2.2.4、设置栈大小
//使用配置,设置栈为1MB,下面可以3选一
- Xss1m
- Xss1024k
- Xss1048576
完整的写法是,-XX:ThreadStackSize=1m
在VM
中配置
2.3、堆(heap
)
2.3.1、概述
HotSpot
是使用指针的方式来访问对象:Java
堆中会存放指向类元数据的地址。Java
栈中的reference
存储的是指向堆中的对象的地址。
- 堆空间概述
- 一个
Java
程序运行起来对应一个进程,一个进程对应一个JVM
实例,一个JVM
实例中有一个运行时数据区。 - 堆是
Java
内存管理的核心区域,在JVM
启动的时候被创建,堆内存的大小是可以调节的。
- 一个
2.3.2、空间划分
1.逻辑上划分
- 空间划分:
Young Generation Space
新生代/年轻代(Young/New
),其又分为两部分。- 伊甸园区(
Eden space
)。 - 幸存者区(
Survivor pace
) 。- 0区(
Survivor 0 space
) - 1区(
Survivor 1 space
)
- 0区(
- 伊甸园区(
Tenured generation space
养老代/老年代(Old/Tenured
)。Permanent Space/Meta Space
永久代/元空间(Permanent/Meta
),又称为非堆。
- 注意:
- 年轻代和老年代占比1:3。
- 伊甸园区和幸存者区占比8比2(0区和1区大小一样。)
- 方法区(具体的实现是永久代和元空间)逻辑上是堆空间的一部分,但是虚拟机的实现中将方法区和堆分开了。
2.图解
2.3.3、分代空间工作流程
1.概述
- 存储在JVM中的Java对象可以被划分为两类:
- 一类是生命周期较短的对象,创建在新生代,在新生代中被垃圾回收。
- 一类是生命周期非常长的对象,创建在新生代,在老年代中被垃圾回收,甚至与
JVM
生命周期保持一致。
- 几乎所有的对象创建在伊甸园区,绝大部分对象销毁在新生代(90%多),大对象直接进入老年代。
- 经常GC的是新生代,偶尔GC的是老年代,几乎不GC的是元空间。
2.新生代
- 工作过程:
- 新创建的对象先放在伊甸园区。
- 当伊甸园的空间用完时,程序又需要创建新对象,此时,触发
JVM
的垃圾回收器对伊甸园区进行垃圾回收(MinorGC
,也叫YoungGC
),将伊甸园区中不再被引I用的对象销毁。 - 然后将伊甸园区的剩余对象移动到空的幸存0区。
- 此时,伊甸园区清空。
- 被移到幸存者0区的对象上有一个年龄计数器,值是1。
三、类加载器CLassLoader
3.1、介绍
JVM
把Class
文件中的类描述数据从文件加载到内存,并对数据进行校验、转换解析、初始化,使这些数据最终成为可以被JVM
直接使用的Java
类型,这个说来简单但实际复杂的过程叫做JVM
的类加载机制。
- 负责加载
class
文件,class文件在文件开头有特定的文件标识(cafe babe
)。 ClassLoader
只负责class
文件的加载,至于它是否可以运行,则由Execution Engine
决定。- 加载的类信息存放到方法区的内存空间。
3.2、类加载过程
3.2.1、介绍
- 类加载过程主要分为三个步骤:加载、链接、初始化,而其中链接过程又分为三个步骤:验证、准备、解析,加上卸载、使用两个步骤统称为类的生命周期。
3.2.2、详细过程
1.加载
- 加载阶段的任务是将类的字节码文件加载到内存中,并在内存中构建出Java类的原型——类模板对象,然后生成对应的 Class 对象。
- 流程(懒加载)
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个字节流代表的静态存储结构转为方法区运行时数据结构。
- 在内存中生成一个代码这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
- 加载的来源:
- 从本地文件系统加载
.class
文件。 - 从
JAR
包中加载。 - 通过网络加载。
- 动态生成字节码(如动态代理)。
- 从本地文件系统加载
- 加载的结果:
- 在方法区(
Metaspace
)中生成类的运行时数据结构(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)。 - 在堆内存中生成对应的
Class
对象(),作为方法区数据的访问入口。
- 在方法区(
2.链接
(1).验证
- 用于确保类或接口的二进制表示结构上是正确的,从而确保字节流包含的信息对虚拟机来说是安全的。验证字节码文件的合法性,确保其符合 JVM 规范。
- 检查内容包括:
- 文件格式验证(魔数、版本号等):主要验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。
- 元数据验证(类、字段、方法的合法性):主要对字节码描述的信息进行语义分析,以保证其提供的信息符合Java语言规范的要求。。
- 字节码验证(方法体的合法性):主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,字节码验证将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
- 符号引用验证(常量池中的符号引用是否可访问):最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段解析阶段发生。符号引用是对类自身以外(常量池中的各种符号引用)的信息进行匹配校验。
(2).准备
- 为类的静态变量分配内存,并设置默认初始值。
static int value = 123;
在准备阶段,value
被初始化为0
,而不是123
。 - 实例变量是在创建对象的时候完成赋值,且实例变量随着对象一起分配到
Java
堆中。 final
修饰的常量在编译的时候会分配,准备阶段直接完成赋值,即没有赋初值这一步。被所有线程所有对象共享。
(3).解析
- 将常量池中的符号引用(
Symbolic Reference
)替换为直接引用(Direct Reference
)的过程。该阶段会把一些静态方法(符号引用 如main()
方法)替换为执行数据所存内存的指针或句柄(直接引用),这就是所谓的 静态链接 过程(类加载期间完成)。动态链接实在程序运行期间完成的将符号引用替换为直接引用(如 实际使用的方法math.compute()
)。- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中。
- 直接引用:直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了。
3.初始化
- 初始化是类加载的最后一步。除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才开始真正执行Java代码。初始化主要工作就是对类的静态变量初始化为指定的值(如
initData = 666
)和执行静态代码块。 - 初始化阶段是执行类构造器
<clinit>
的过程。这一步主要的目的是根据程序员程序编码制定的主观计划去初始化类变量和其他资源。 - 初始化的触发条件:
- 创建类的实例。
- 访问类的静态成员。
- 调用类的静态方法。
- 反射调用类。
- 子类初始化时,父类会优先初始化。
- 初始化的过程:
- 执行静态变量的赋值操作。
- 执行静态代码块(static {})。
- 如果存在父类,优先初始化父类。
- 注意:主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。
3.3、类加载器作用
- 负责加载
class
文件,class
文件在文件开头有的文件标识(CA FE BA BE
),并且classLoader
只负责class
文件的加载,至于它是否可以运行,则由Execution Engine
决定。
3.4、类加载器分类
3.4.1、介绍
Bootstrap ClassLoader
(启动类加载器):负责加载JVM
核心类库(如java.lang.*
、java.util.*
等),这些类通常位于JAVA_HOME/lib
目录下不继承java.lang.ClassLoader
,是扩展类加载器的父加载器(不是父类)。由C/C++
实现,是JVM
的一部分,不是Java
类。在java代码中无法直接获取到引用,返回null。Extension ClassLoader(PlatformClassLoader)
(扩展类加载器):负责加载JAVA_HOME/lib/ext
目录下的类库,或者由java.ext.dirs
系统属性指定的路径。由Java
实现,是sun.misc.Launcher$ExtClassLoader
类的实例。由java代码编写,继承ClassLoader
类。Application ClassLoader
(应用程序类加载器):负责加载用户类路径(ClassPath
)下的类库。由 Java 实现,是sun.misc.Launcher$AppClassLoader
类的实例。由java代码编写,继承ClassLoader类,为程序中默认的类加载器。- 自定义类加载器:用户可以通过继承
ClassLoader
类实现自定义类加载器,用于加载特定路径或来源的类。支持一些个性化的功能。自定义类加载器只需继承java.lang.ClassLoader
类,该类有两个核心方法,一个是loadClass(String, boolean)
,实现了双亲委派机制,还有一个方法是findClass
,默认实现是空方法,所以我们自定义类加载器主要是重写findClass
方法。
3.4.2、版本变化
Java 9
之前的classLoader
Bootstrap ClassLoader
加载$JAVA_HOME
中jre/lib/rt.jar
,加载JDK
中的核心类库ExtClassLoader
加载相对次要、但又通用的类,主要包括$JAVA_HOME
中jre/lib/*.jar
或-Djava.ext.dirs
指定目录下的jar包。AppClassoader
加载-cp(classpath)
指定的类,加载用户类路径中指定的jar包及目录中class
Java 9
及之后的classLoader
Bootstrap ClassLoader
,使用了模块化设计,加载lib/modules
启动时的基础模块类java.base
、java.management
和java.xml
。ExtClassLoader
更名为PlatformClassLoader
,使用了模块化设计,加载lib/modules
中平台相关模块,如java.scripting
、java.compiler
。AppClassLoader
加载-cp(classpath)
,-mp(modelpath)
指定的类,加载用户类路径中指定的jar
包及目录中class
3.4.3、类加载器的层级关系
注意,这里的父子关系并不是代码中的extends
的关系,而是逻辑上的父子。
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoaderDemo classLoaderDemo = new ClassLoaderDemo();
//AppClassLoader
ClassLoader appClassLoader = classLoaderDemo.getClass().getClassLoader();
System.out.println(ClassLoader.getSystemClassLoader());
//PlatformClassLoader(Extension ClassLoader)
ClassLoader platformClassLoader = appClassLoader.getParent();
System.out.println(platformClassLoader);
//BootstrapClassLoader
ClassLoader bootstrapClassLoader = platformClassLoader.getParent();
System.out.println(bootstrapClassLoader);
//BootstrapClassLoader
String s = new String();
System.out.println(s.getClass().getClassLoader());
}
}
3.5、类加载机制
3.5.1、介绍
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。
- 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
3.5.2、双亲委派机制
- 双亲委派机制是类加载器的一种工作模式,其核心思想是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。如果父类加载器无法加载,子类加载器才会尝试加载。
- 工作流程
- 当
AppClassLoader
加载一个class
时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器PlatformClassLoader
去完成。 - 当
PlatformClassLoader
加载一个class
时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器BootStrapClassLoader
去完成。 - 如果
BootStrapClassLoader
加载失败,会用PlatformClassLoader
来尝试加载。 - 若
PlatformClassLoader
也加载失败,则会使用AppClassLoader
来加载。 - 如果
AppClassLoader
也加载失败,则会报出异常ClassNotFoundException
。
- 当
- 目的
- 保证核心类库的安全性:核心类库(如
java.lang.*
)由启动类加载器加载,防止用户自定义类替换核心类。 - 保证类的唯一性:同一个类在不同的类加载器中会被视为不同的类,双亲委派机制确保类的唯一性。
- 保证核心类库的安全性:核心类库(如
四、垃圾回收
4.1、介绍
Java
中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection
简称GC
)机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。其他很多现代语言比如c#
、Python
、Go
都拥有自己的垃圾回收器。- 线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。因此线程不共享的程序计数器、虚拟机栈、本地方法栈中没有垃圾回收。
JVM
(Java
虚拟机)的垃圾回收机制(Garbage Collection
,GC
)是Java
内存管理的核心部分。它负责自动回收不再使用的对象,释放内存空间,从而避免内存泄漏和手动内存管理的复杂性,垃圾收集主要是针对堆和方法区进行。- 垃圾:是指程序中不再被引用的对象。这些对象占用的内存可以被回收。
Stop-the-world
:意味着JVM
由于要执行GC
而停止了应用程序的执行,并且这种情形会在任何一种GC
算法中发生,
当Stop-the-world
发生时,除了GC
所需的线程以外,所有线程都处于等待状态直到GC
任务完成。事实上,GC
优化很多时候就是指减少Stop-the-world
发生的时阅。从而使系统具有高吞吐,低停顿的特点。
4.2、方法区垃圾回收
4.2.1、类和对象的生命周期
1.类的生命周期
Java
类的生命周期从类加载开始,到类卸载结束。
- 加载(
Loading
):类加载器将类的字节码文件(.class
文件)加载到内存中。 - 链接(
Linking
)- 验证(
Verification
):验证字节码文件的合法性,确保其符合JVM
规范。 - 准备(
Preparation
):为类的静态变量分配内存,并设置默认初始值。 - 解析(
Resolution
):将常量池中的符号引用替换为直接引用。
- 验证(
- 初始化(
Initialization
):执行类的静态变量赋值和静态代码块。 - 使用(
Usage
):类被程序使用,可能用于创建对象、调用静态方法、访问静态变量等。 - 卸载(
Unloading
):当类的Class
对象不再被引用,会被卸载,卸载后,类的元数据(如方法区中的信息)会被清除。- 类的所有实例都已被回收。
- 类的
ClassLoader
已被回收。 - 类的
Class
对象不再被引用。
2.对象的生命周期
Java 对象的生命周期从创建开始,到被垃圾回收结束。
- 创建(
Creation
):对象通过new
关键字创建,JVM
为对象分配内存,并调用构造函数初始化对象。 - 使用(
Usage
):对象被程序使用,可能被赋值给变量、作为参数传递、调用方法 - 不可见阶段(
Invisible
):对象失去了引用,程序已经无法通过任何方式访问到它(还没被不可达算法标记)。 - 不可达(
Unreachable
):当对象不再被任何引用指向时,它变为不可达对象。 - 收集阶段/垃圾回收(
Garbage Collection
):垃圾回收器(GC
)会定期检查堆内存,回收不可达对象。这个阶段可能会复活对象(如下图)。 - 终结(
Finalization
):在对象被回收之前,JVM
会调用其 finalize() 方法(如果重写了该方法)。
4.2.2、回收
- 方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:
- 此类所有实例对象没有在任何地方被引用,在堆中不存在任何该类的实例对象以及子类对象。
- 该类对应的
java.lang.Class
对象没有在任何地方被引用。 - 加载该类的类加载器没有在任何地方被引用。
- 方法区的回收通常情况下很少发生,但是如果通过自定义类加载器加载特定的是少数的类,那么可以在程序中释放自定义类加载器的引用,卸载当前类,垃圾回收及会对这部分内容进行回收。
4.3、判断垃圾的方法
4.3.1、可达性分析算法(Reachability Analysis
)
通过一系列称为GC Roots
的根对象作为起点,从这些根对象出发,遍历所有可达的对象,未被遍历到的对象则被视为垃圾。可达性分析算法的分析工作必须在一个保障一致性的快照中进行,否则结果的准确性无法保证,这也是导致GC
进行时必须Stop The World
的一个原因。
可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链,如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象,在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
- 流程:
- 确定
GC Roots
:GC Roots
是一组不会被回收的根对象, - 遍历对象图:从
GC Roots
出发,递归遍历所有可达的对象,并标记这些对象为存活对象。 - 清除不可达对象:未被标记的对象则被视为不可达对象,可以被回收。
- 确定
4.3.2、引用计数法(Reference Counting
)
引用计数法是另一种判断对象是否为垃圾的方法,但其在Java
中并未被使用。它对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A
,只要有任何一个对象引用了A
,则A
的引用计数器就加1
;当引用失效时,引用计数器就减1
;当对象A
的引用计数器的值为0
,即表示对象A
不可能再被使用,可进行回收(Java
没有采用)。
- 优点:
- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为
0
,可以直接回收 - 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报
OOM
错误 - 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象
- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为
- 缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销
- 浪费
CPU
资源,即使内存够用,仍然在运行时进行计数器的统计。 - 无法解决循环引用问题,会引发内存泄露(最大的缺点)
class A {
B b;
}
class B {
A a;
}
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
此时,a
和b
相互引用,但没有任何外部引用指向它们,引用计数法无法回收它们。
4.3.3、三色标记的原理(Tri-color marking
)
三色标记法(Tri-color Marking)是一种用于垃圾回收的算法,主要用于标记阶段,帮助垃圾回收器高效地遍历和标记所有可达对象。它是现代垃圾回收器(如 G1、CMS、ZGC 等)的核心算法之一。三色标记法通过将对象标记为三种颜色(白色、灰色、黑色)来管理对象的标记状态,从而确保垃圾回收的正确性和高效性。
- 颜色概述
- 白色:表示对象尚未被垃圾回收器扫描过。在标记开始时,堆内存中的对象都是白色的。在标记结束时,仍然是白色的对象,将会被视为已死的对象而被清除。
- 灰色:灰色是由白色标记成为灰色,表示该对象
Obj
是根可达对象(存活的对象,不会被清理),但Obj
的至少有一个引用的对象还没被垃圾收集器访问;灰色是一个过渡色,最终都会被标记为黑色。 - 黑色:黑色是由灰色标记成为黑色,表示该对象
Obj
以及Obj
的所有下级引用对象都已经被垃圾收集器访问并标记,Obj
不会被垃圾收集器再次访问以查看是否有引用对象;此时Obj
是黑色(存活的对象,不会被清理),Obj
的所有引用的对象被标记为灰色。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 标记过程
- 初始时,所有对象都在【白色集合】中。
- 将
GCRoots
直接引用到的对象挪到【灰色集合】中 - 从灰色集合中获取对象。
- 将本对象引用到的其他对象全部挪到【灰色集合】中
- 将本对象挪到【黑色集合】里面。
- 重复步骤345,直至【灰色集合】为空时结束。
- 结束后,仍在【白色集合】的对象即为GCRoots不可达,可以进行回收
- 产生问题
-
多标-浮动垃圾:在并发标记过程中,如果由于方法运行结束导致部分局部变量(
gcroot
)被销毁,这个gcroot
引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC
不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。 -
如下图,当E对象扫描完后,D断开其引用,E,G会被标记为灰色最后标记为黑色,产生浮动垃圾。
-
漏标-读写屏障:漏标只有同时满足以下两个条件时才会发生。如下图,在D被扫描完,将E标记为灰色后,断开E对G的引用,新增了D对G的引用,导致G无法被标记,但其正在被使用,垃圾回收会将其回收,从而产生空指针问题。
-
条件一:灰色对象断开了白色对象的引用,即灰色对象原来成员变量的引用发生了变化。
-
条件二:黑色对象重新引用了该白色对象,即黑色对象成员变量增加了新的引用。
-
漏标会导致被引用的对象被当成垃圾误删除,这是严重
bug
,必须解决,有两种解决方案:增量更新(Incremental Update
) 和原始快照 (Snapshot At The Beginning, SATB
)。 -
增量更新(破坏条件2):就是当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
-
原始快照(破坏条件1):就是当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
-
写屏障实现原始快照(
SATB
):当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d= null)
,我们可以利用写屏障,将B原来成员变量的引用对象D记录下来。 -
写屏障实现增量更新:当对象A的成员变量的引用发生变化时,比如新增引用(
a.d = d
),我们可以利用写屏障,将A新的成员变量引用对象D记录下来。
-
-
4.4、GC Roots概念
GC Roots
是Java
垃圾回收机制中的一个核心概念,它是垃圾回收器判断对象是否存活的起点。GC Roots
是一组特殊的对象引用,从这些引用出发,垃圾回收器可以遍历整个对象图,标记所有可达的对象,未被标记的对象则被视为垃圾,可以被回收。- 作用
- 标记阶段:从
GC Roots
出发,遍历所有可达对象,并标记这些对象为存活对象。未被标记的对象则被视为垃圾。 - 清除阶段:清除所有未被标记的对象,释放其占用的内存。
- 标记阶段:从
- 在Java语言中,可以作为GC Root的对象包括下面几种:
- 虚拟机栈中的局部变量:当前正在执行的方法中的局部变量引用的对象(栈帧局部变量表中的
reference
引用所引用的对象)。 - 方法区中
static
静态引用的对象。 - 方法区中
final
常量引用的对象(常量池中的常量引用的对象)。 - 本地方法栈中的
JNI
引用:通过JNI(Java Native Interface)
调用的本地方法引用的对象。 Java
虚拟机内部的引用:JVM
内部的一些特殊对象引用,如系统类加载器、基本类型的Class
对象等。- 同步锁持有的对象:被同步锁(
synchronized
)持有的对象。
- 虚拟机栈中的局部变量:当前正在执行的方法中的局部变量引用的对象(栈帧局部变量表中的
4.5、垃圾回收算法
4.5.1、标记-清除算法(Mark-Sweep
)
- 标记-清除算法是最基本的垃圾回收算法,分为两个阶段。
- 标记阶段:从根集合(
GC Roots
)开始,递归标记所有可达的对象,使用根可达算法。 - 清除阶段:对堆内存从头到尾进行线性便遍历,如果发现某个对象没有被标记为可达对象,则将其回收。未被标记的对象就是未被引用的垃圾对象。
- 优点:简单。
- 缺点:
- 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收。
- 效率较低,扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)。
- 标记阶段:从根集合(
4.5.1、标记-复制算法(Copying
)
- 将内存分为两块,每次只使用一块,将存活的对象复制到另一块内存,并清除当前内存块。
- 核心思想
- 将堆内存分割成两块
From
空间To
空间,对象分配阶段,创建对象。 GC
阶段开始,将GCRoot
搬运到To
空间。- 将
GCRoot
关联的对象,搬运到To
空间。 - 清理
From
空间,并把名称互换。
- 将堆内存分割成两块
- 图解:绿色存活对象/红色回收垃圾,激活区-
from
/空闲区-to
- 优点
- 无内存碎片。
- 扫描了整个空间一次(标记存活对象并复制移动)。
- 实现简单。
- 缺点
- 内存利用率低(只有一半内存可用),如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
- 如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将
所有引用地址重置一遍,复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
4.5.3、标记-压缩(整理)算法(Mark-Compact
)
-
标记所有需要回收的对象,将存活的对象向一端移动,清理边界以外的内存。
- 标记阶段:与标记-清除一样、
- 压缩阶段:再次扫描,并往一端滑动存活对象。
-
标记整理算法是标记-清除法的一个改造版,同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是通过所有存活对像都间一增移动,然后直接清除边界以外的内存。
-
优点:标记整理算法不仅可以弥补标记清除算法中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。
-
缺点:如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
4.5.4、分代收集算法(Generational Collection
)
1.介绍
- 根据对象的生命周期将内存分为新生代和老年代,新生代使用复制算法,老年代使用标记-清除或标记-整理算法。
- 优点:结合了不同算法的优点,提高了垃圾回收效率。
- 对比
- 内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
- 内存整齐度:复制算法>标记整理算法>标记清除算法。
- 内存利用率:整理算法=清除算法>复制算法。
- 可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。为了尽量兼顾上面所提到的三个指标,标记整理算法相对来说更平滑一些,但效案上依然不尽如人意。比复制算法多了一个标记的阶段,又比标记清除多了一个整理内存的过程。
- 分代回收算法实际上是复制算法和标记整理法,标记清除的结合,并不是真正一个新的算法。一般分为老年代(
Old Generation
)和年轻代(Young Generation
)老年代就是很少垃圾需要进行回收的,年轻代就是有很多的内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。- 年轻代 (
Young Gen
):年轻代特点是区域相对老年代较小。对像存活率低。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot
中的两个survivor
的设计得到缓解。 - 老年代(
Tenure Gen
):老年代的特点是区域较大,对像存活率高。这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。
- 年轻代 (
2.java堆分代设计的原理
Java 堆的分代设计是 JVM 垃圾回收机制的核心部分之一。分代设计的目的是根据对象的生命周期将堆内存划分为不同的区域,从而优化垃圾回收的效率。
- 对象的生命周期:
- 大部分对象的生命周期很短,创建后很快就会被回收。
- 少数对象的生命周期较长,会长期存活。
- 分代设计的优势:
- 将堆内存划分为新生代和老年代,针对不同生命周期的对象采用不同的垃圾回收策略。
- 提高垃圾回收效率,新生代使用复制算法,回收效率高。
- 老年代使用标记-清除或标记-整理算法,减少内存碎片。
- 减少停顿时间,新生代的
Minor GC
通常较快,对程序的影响较小,老年代的Major GC
或Full GC
虽然较慢,但频率较低。 - 优化内存使用,根据对象的生命周期划分内存区域,提高内存利用率。
4.6、引用类型
4.6.1、强引用(Strong Reference
)
- 最常见的引用类型,通过 new 关键字创建的对象默认是强引用,如
Object obj = new Object()
。只要强引用存在,垃圾回收器就不会回收被引用的对象。即使内存不足,JVM
也会抛出OutOfMemoryError
,而不是回收强引用对象。
public class StrongReferenceTest {
public static void main(String[] args) {
//定义强引用
User user = new User(1, "zhangsan");
//定义强引用
User user1 = user;
//设置user为null,User对象不会被回收,因为依然被user1引用
user = null;
//强制垃圾回收
System.gc();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(user1);
}
}
4.6.2、软引用(Soft Reference
)
SoftReference
类实现软引用。在系统要发生内存溢出(OOM
)之前,才会将这些对象列进回收范围之中进行二次回收,如果这次回收还没有定够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存,例如:EHCache
这样的本地缓存框架,还有Netty
这样的异步网络通信框架。
//-Xms10m -Xmx10m
public class SoftReferenceTest {
public static void main(String[] args) {
//创建对象,建立软引用
SoftReference<User> userSoftRef = new SoftReference<>(new User(1, "zhangsan"));
//上面的一行代码,等价于如下的三行代码
//User u1 = new User(1,"zhangsan");
//SoftReference<User> userSoftRef = new SoftReference<>(u1);
//u1 = null;//如果之前定义了强引用,则需要取消强引用,否则后期userSoftRef无法回收
//从软引用中获得强引用对象
System.out.println(userSoftRef.get());
//内存不足测试:让系统认为内存资源紧张
//测试环境: -Xms10m -Xmx10m
try {
//默认新生代占堆的1/3空间,老年代占堆的2/3空间,因此7m的内容在哪个空间都放不下
byte[] b = new byte[1024 * 1024 * 8]; //7M
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println("finally");
//再次从软引用中获取数据
//在报OOM之前,垃圾回收器会回收软引用的可达对象。
System.out.println(userSoftRef.get());
}
}
}
4.6.3、弱引用(Weak Reference
)
- 通过
WeakReference
类实现。垃圾回收器在每次垃圾回收时都会回收弱引用指向的对象。对象只能生存到下一次垃圾收集(GC)
之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
public class WeakReferenceTest {
public static void main(String[] args) {
//构造了弱引用
WeakReference<User> userWeakRef = new WeakReference<>(new User(1, "zhangsan"));
//从弱引用中重新获取对象
System.out.println(userWeakRef.get());
System.gc();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 不管当前内存空间足够与否,都会回收它的内存
System.out.println("After GC:");
//重新尝试从弱引用中获取对象
System.out.println(userWeakRef.get());
}
}
4.6.4、虚引用(Phantom Reference
)
- 也叫幽灵引用、幻影引用。
- 通过
PhantomReference
类实现。它是最弱的一种引用关系。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,主要用于执行一些清理操作或监视对象的回收状态。虚引用不会影响对象的生命周期。 - 对象回收跟踪
PhantomReference
类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,它主要用于执行一些清理操作或监视对象的回收状态。- 虚引用与软引用和弱引用的一个区别在于:
- 虚引用必须和引用队列 (
ReferenceQueue
)联合使用。 - 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
- 虚引用必须和引用队列 (
- 举例:创建了一个
PhantomReference
对象phantomRef
,它引用了一个User
实例obj
。当解除obj
的强引用后,obj
将成为垃圾回收的候选对象。然后通过调用System.gc()
方法建议垃圾回收器执行回收操作,并等待一段时间以确保垃圾回收完成。最后,使用isEnqueued()
方法判断虚引用是否已经被回收。
public class PhantomReferenceTest {
public static void main(String[] args) {
User obj = new User(1, "zhangsan");
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
obj = null; // 解除强引用
// 在这里,对象可能已经被垃圾回收了,但我们无法通过虚引用获取它
// 判断虚引用是否被回收
boolean isCollected = false;
while (!isCollected) {
System.gc(); // 建议垃圾回收器执行回收操作
try {
Thread.sleep(1000); // 等待1秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
if (phantomRef.isEnqueued()) { //判断虚引用是否已经被回收。
isCollected = true;
}
}
// 输出虚引用是否被回收
System.out.println("虚引用是否被回收:" + isCollected);
}
}
4.7、垃圾回收器分类(JVM默认的)
GC发展史:有了Java虚拟机,就需要收集垃圾的机制,这就是GC(Garbage Collection
),对应的产品我们称为Garbage Collector
。
- 1999年随
JDK1.3.1
一起来的是串行方式的serial Gc
,它是第一款GC。ParNew
垃圾收集器是Serial
收集器的多线程版本。 - 2002年2月26日,
Parallel Gc
和Concurrent MarkSweepC
跟随JDK1.4.2
一起发布。 Parallel GC
在JDK6
之后成为Hotspot
默认GC
。- 2012年,在
JDK1.7u4
版本中,G1可用。 - 2017年,JDK9中G1变成默认的垃圾收集器,以替代
CMS
。 - 2018年3月,
JDK 10
中G1
垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。 - 2018年9月,
JDK11
发布。引入Epsilon
垃圾回收器,又被称为"No-0p(无操作)"回收器。同时,引入zGC
:可伸缩的低延迟垃圾回收器(Experimental
)。 - 2019年3月,
JDK12
发布。增强G1
,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC
:低停顿时间的GC(Experimental)
。 - 2020年3月,JDK14发布。删除
CMS
垃圾回收器。扩展ZGC
在MacOS
和Windows
上的应用。 - 查看默认垃圾回收器信息
java -XX:+PrintCommandLineFlags -version
4.7.1、Serial
和Serial Old
收集器
1.介绍
Serial 系列的垃圾收集器是Java 虚拟机中最早的一批收集器之一。它们的设计初衷是为了适应早期的硬件环境和应用场景。在那个时候,硬件配置相对较低,主要特点包括内存容量较小、CPU 单核、并发应用场景相对较少。基于这些限制条件,Serial 系列的垃圾收集器采用了简单高效、资源消耗最少、单线程收集的设计思路,内存在几M - 几十M
的场景下。
2.特点
- 简单高效:由于硬件资源有限,垃圾回收器需要设计得简单高效,以减少系统资源的占用。Serial 系列的垃圾收集器实现简单,适用于小型应用或者简单的测试场景。
- 资源消耗最少:考虑到当时硬件资源有限,Serial 系列的垃圾收集器尽可能地减少了对系统资源的占用。通过使用单线程执行垃圾回收操作,避免了多线程切换的开销,从而最大程度地节约了系统资源。
- 单线程收集:由于早期的硬件环境和应用场景下,并发需求较低,采用单线程收集的设计方案足以满足当时的需求。单线程收集简化了垃圾回收器的实现,并降低了系统复杂性,使得垃圾回收过程更加可控和稳定。
3.垃圾收集流程
Serial
垃圾收集过程的简单之处在于其采用了单线程执行的方式,以简化实现并减少资源占用。- 暂停用户线程(
Stop the World
):在开始垃圾收集过程之前,Serial
垃圾收集器会暂停(停止)所有的用户线程。这是为了确保在垃圾收集过程中对象的状态不会被修改,从而保证垃圾收集的准确性。 - 执行垃圾收集:一旦用户线程暂停,
Serial
垃圾收集器会开启一个单线程来执行垃圾回收操作。这个线程会遍历堆中的对象,标记并清理不再使用的对象,以释放内存空间。 - 等待垃圾收集完成:在垃圾收集过程中,用户线程会被暂停,直到垃圾收集完毕。这意味着用户线程无法在垃圾收集期间执行任何操作。
- 恢复用户线程:当垃圾收集完成后,
Serial
垃圾收集器会恢复用户线程的执行。此时,垃圾已被清理,堆内存中有更多的可用空间供应用程序使用。
- 暂停用户线程(
4.图解
5.补充
- 注意:“暂停用户线程”,这里也是各种垃圾收集器的一个区分指标,后面的有些垃圾收集器收集的某些阶段是不需要暂停用户线程的。
- 补充:
- 收集区域:
Serial
(新生代),Serial Old
(老年代) - 使用算法:
Serial
(标记复制法),Serial Old
(标记整理法) - 搜集方式: 单线程收集
- 收集区域:
4.7.2、Parallel Scavenge
和 Parallel Old
收集器
1.介绍
随着硬件资源的升级,包括内存空间的增大和 CPU 的多核化,传统的Serial
垃圾收集器面临着性能瓶颈。由于它采用单线程执行垃圾回收操作,无法充分利用多核CPU
的优势,导致在处理大内存空间时性能下降,垃圾回收时间变得更长。为了充分发挥多核CPU
的优势,JVM
推出了Parallel
收集器系列。Parallel
收集器的设计思想是利用多线程并行执行垃圾回收操作,以提高整个垃圾收集过程的并行度和性能,内存在几十M - 几G的场景下。
2.特点
- 多线程并行执行:
Parallel
收集器利用了多核CPU
的优势,通过多个线程同时执行垃圾回收操作,加快了垃圾收集的速度。 - 高吞吐量:由于并行执行垃圾收集操作,
Parallel
收集器适用于吞吐量要求较高的应用场景。它能够在保证吞吐量的同时,尽可能地减少垃圾收集的停顿时间。 - 适用于大内存堆:随着内存空间的扩大,Parallel 收集器能够更好地应对大内存堆的情况,通过并行执行垃圾收集操作,提高了整个垃圾收集过程的效率。
- 相比于传统的
Serial
收集器,Parallel
收集器能更好地适应现代应用的需求,特别是大型内存堆和高吞吐量的场景。
3.垃圾收集流程
Parallel Scavenge
和Parallel Old
是Parallel
收集器系列的两个组成部分,它们的工作机制相似,都是利用多线程并行执行垃圾回收操作,以提高整个垃圾收集过程的效率和吞吐量。以下以Parallel Scavenge
为例来说明其工作机制:- 多线程并行执行:
Parallel Scavenge
收集器利用了多个线程并行执行新生代的垃圾回收操作。这意味着在进行新生代垃圾回收时,多个线程同时工作,加快了垃圾收集的速度。 - 暂停用户线程:与
Serial
收集器类似,Parallel Scavenge
在进行垃圾收集时会暂停用户线程,以确保垃圾回收的准确性。这一阶段通常称为“Stop the World
”。 - 多线程并发清理:
Parallel Scavenge
收集器的特点之一是在新生代垃圾收集过程中采用并行清理(Parallel Cleaning
)的方式。这意味着在暂停用户线程期间,多个线程同时清理新生代中的垃圾对象,从而更快地完成垃圾收集过程。 - 高效利用多核
CPU
:通过利用多个线程并行执行垃圾收集操作,Parallel Scavenge
能够充分发挥多核CPU
的优势,提高了垃圾收集的效率。相比于Serial
收集器,它能更快地完成垃圾回收操作,从而减少了应用程序的停顿时间,从而提高了整个应用程序的性能。
- 多线程并行执行:
4.图解
5.补充
- 收集区域: `Parallel Scavenge`(新生代),`Parallel Old`(老年代)
- 使用算法: `Parallel Scavenge`(标记复制法),`Parallel Old`(标记整理法)
- 搜集方式: 多线程
4.7.3、ParNew
收集器
ParNew
和Parallel Scavenge
垃圾收集器在实现上确实有一些相似之处,都属于并行垃圾收集器。但ParNew
垃圾收集器之所以出名,一个重要原因是它是唯一能与CMS(Concurrent Mark-Sweep)
收集器配合使用的新生代收集器,特别适用于那些对停顿时间要求较高的应用场景。
- 特点/优势:
- 与
CMS
配合:ParNew
垃圾收集器能够与CMS
垃圾收集器配合使用,用于处理老年代的垃圾回收。在这种组合中,ParNew
负责新生代的垃圾收集,而CMS
负责老年代的并发垃圾收集。这种分工合作可以有效地减少应用程序的停顿时间,满足对低停顿时间的需求。 - 并行收集:
ParNew
垃圾收集器采用多线程并行收集的方式,类似于Parallel Scavenge
收集器。它能够充分利用多核CPU
的优势,加快垃圾收集的速度,提高整个应用程序的性能。 - 应对停顿时间要求高的场景:由于
ParNew
与CMS
配合使用,可以针对那些对停顿时间要求较高的应用场景。CMS
收集器通过并发执行垃圾回收操作,尽量减少停顿时间,而ParNew
则能够在新生代中高效地执行垃圾回收操作,进一步降低停顿时间。
- 与
- 垃圾收集流程:
ParNew
收集器和Parallel Scavenge
收集器在工作流程上确实非常相似,都是并行垃圾收集器。- 停止应用程序线程(
Stop the World
):在进行垃圾收集之前,ParNew
收集器会暂停所有的用户线程。这一阶段被称为停止应用程序线程,以确保在垃圾收集过程中对象的状态不会被修改,保证垃圾回收的准确性。 - 多线程并行执行垃圾收集:一旦应用程序线程暂停,
ParNew
收集器会启动多个线程并行执行垃圾回收操作。这些线程会同时在新生代中扫描和清理不再使用的对象,以释放内存空间。 - 暂停用户线程:在整个垃圾收集过程中,用户线程会一直处于暂停状态,直到垃圾收集完成。这个阶段也被称为“
Stop the World
”,在此期间应用程序无法执行任何操作。 - 恢复用户线程:当垃圾收集完成后,
ParNew
收集器会恢复用户线程的执行。此时,垃圾已经被清理,堆中的内存空间得到了释放,用户线程可以继续执行。
ParNew
收集器的工作流程与Parallel Scavenge
收集器类似,都是通过停止应用程序线程,然后利用多线程并行执行垃圾回收操作,最后恢复用户线程的执行。这种并行执行的方式能够提高垃圾收集的效率,同时在暂停用户线程期间确保垃圾收集的准确性。
- 停止应用程序线程(
- 注:
- 收集区域: 新生代
- 使用算法: 标记复制法
- 搜集方式: 多线程。
4.7.4、CMS收集器(Concurrent Mark-Sweep
)
1.介绍
- 随着硬件技术的发展,可用内存越来越大,这为应用程序提供了更多的内存空间,从而能够创建更多的对象,减少了垃圾收集的频率。然而,随着内存空间的增大,垃圾收集的时间也相应增加,可能导致长时间的停顿,影响用户体验。在这种情况下,传统的垃圾收集器需要暂停应用程序线程进行垃圾收集,这会导致用户在执行某些操作时出现延迟甚至停顿的情况,这是无法接受的。
CMS
垃圾收集器的设计初衷是允许垃圾收集器在进行垃圾回收的同时,与应用程序的线程并发执行,不需要长时间暂停应用程序线程。CMS
垃圾收集器通过并发标记和清除的方式,允许在垃圾收集过程中与应用程序并发执行,从而降低了垃圾收集的停顿时间,提高了系统的响应性和用户体验。
2.特点
- 并发标记和清除:
CMS
垃圾收集器采用了并发标记和清除的方式,允许在垃圾收集过程中与应用程序并发执行。这意味着垃圾收集过程中只有一小部分时间需要暂停应用程序线程。 - 低停顿时间:由于并发执行的特性,
CMS
垃圾收集器能够在较短的时间内完成垃圾回收操作,从而减少了应用程序的停顿时间。通常情况下,CMS
垃圾收集器能够将停顿时间控制在几百毫秒甚至更低。 - 老年代收集:
CMS
垃圾收集器主要针对老年代进行垃圾回收,对于新生代则通常使用ParNew
收集器。这种分代收集的方式能够更好地适应不同内存区域的特点和垃圾回收需求。
3.垃圾收集流程
CMS(Concurrent Mark-Sweep)
垃圾收集器为了尽量减少用户线程的停顿时间,采用了一种创新的策略。这一策略使得在垃圾回收过程的某些阶段,用户线程和垃圾回收线程可以共同工作,从而避免了长时间的垃圾回收导致用户线程一直处于等待状态。- 初始标记(
Initial Mark
):在这个阶段,CMS
垃圾收集器会对根对象(Gc Roots
)进行一次快速的标记,标记出所有与根对象直接关联的存活对象。这个阶段需要暂停用户线程,因为要确保标记的准确性。 - 并发标记(
Concurrent Mark
):在这个阶段,CMS
垃圾收集器会与用户线程并发执行,对整个堆进行标记。垃圾回收线程会在后台标记所有存活对象,而用户线程可以继续执行,不受影响。 - 重新标记(
Remark
):在并发标记阶段结束后,CMS
垃圾收集器会进行一次重新标记,来处理在并发标记阶段发生变化的对象。这个阶段需要暂停用户线程,以确保标记的准确性。 - 并发清理(
Concurrent Sweep
):在重新标记完成后,CMS
垃圾收集器会与用户线程并发执行,清理未标记的对象。垃圾回收线程会在后台清理不再使用的对象,而用户线程可以继续执行,不受影响。
- 初始标记(
通过将垃圾回收过程分为多个阶段,并在其中允许用户线程和垃圾回收线程并发执行,CMS
垃圾收集器成功地减少了用户线程的停顿时间。这种创新的并发垃圾收集策略提高了系统的响应性和用户体验,确保了应用程序的顺畅运行。
4.图解
5.缺点
CMS
收集器对CPU
抢夺过于凶狠吞吐量会降低- 面向并发设计的程序都对
CPU
资源比较凶狠。在并发时它虽然不会导致用户线程停顿但会因为占用一部分线程而导致应用程序变慢总吞吐量会降低。 CMS
默认启动的回收线程数是**(处理器核心数量 +3)/4,也就是说, 如果处理器核心数在四个或以上, 并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源, 并且会随着处理器核心数量的增加而下降。 但是当处理器核心数量不足四个时,CMS
对用户程序的影响就可能变得很大。 如果应用本来的处理器负载就很高, 还要分出一半的运算能力去执行收集器线程, 就可能导致用户程序的执行速度忽然大幅降低。
- 面向并发设计的程序都对
CMS
收集器无法处理浮动垃圾或出现"Concurrent Mode Failure
"失败而导致另一次Full GC
的产生。 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS
无法在当次收集中处理掉它们,只好留待下一次GC
时再清理掉。这一部分垃圾就称为"浮动垃圾"。同样也是由于在垃圾收集阶段用户线程还需要持续运行, 那就还需要预留足够内存空间提供给用户线程使用, 因此CMS
收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集, 必须预留一部分空间供并发收集时的程序运作使用。- 空间碎片:CMS基于标记-清除算法实现的,会有空间碎片的现象。当空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次
Full GC
。
6.补充
- 收集区域: 老年代。
- 使用算法: 标记清除法+标记整理法。
- 搜集方式: 多线程。
4.7.5、G1收集器(Garbage-First
)
1.介绍
CMS
垃圾收集器开创了垃圾收集器的一个新时代,实现了垃圾收集和用户线程同时执行,从而达到了垃圾收集的过程不停止用户线程的目标。这种并发垃圾收集的思路为后续垃圾收集器的发展提供了重要的参考。 随着硬件资源的不断升级,可用的内存资源越来越多,这对于垃圾收集器的发展提出了新的挑战。传统的垃圾收集器采用物理分区的方式将内存分为老年代、新生代、永久代或MetaSpace
,但随着可用内存的增加,某一分代区域的大小可能会达到几十上百GB
。在这种情况下,传统的物理分区收集方式会导致垃圾扫描和清理时间变得更长,性能下降。G1
垃圾收集器摒弃了传统的物理分区方式,而是将整个内存分成若干个大小不同的Region
区域。每个Region
在逻辑上组合成各个分代,这样做的好处是可以以Region
为单位进行更细粒度的垃圾回收。G1
垃圾收集器在进行垃圾回收时,可以针对单个或多个Region
进行回收,从而提高了收集效率和性能。G1
垃圾收集器吸取了CMS
垃圾收集器的优良思路,并通过摒弃物理分区、采用Region
分区的方式,实现了更细粒度的垃圾回收,从而提高了整个系统的性能和可用性。G1
垃圾收集器在大内存环境下的表现更加出色,成为了现代Java
应用中的重要选择。G1
是Java9
以后默认的垃圾收集器,一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。G1收集器的设计目标是取代CMS收集器
2.收集方式
Region
(局部收集):G1
垃圾收集器的最核心分区基本单位是Region
。与传统的垃圾收集器不同,G1
不再将堆内存划分为固定连续的几块区域,而是完全舍弃了物理分区,而是将堆内存拆分成大小为1MB
到32MB
的Region
块。然后,以Region
为单位自由地组合成新生代、老年代、Eden
区、Survivor
区和大对象区(Humongous Region
)等。随着垃圾回收和对象分配的进行,每个Region
也不会一直固定属于某个分代,它们可以随时扮演任何一个分代区域的内存角色。Collect Set
(智能收集):在G1
里面会维护一个Collect Set
集合。这个集合记录了待回收的Region
块的信息,包括每个Region
块可回收的大小空间。有了这个CSet
信息,G1
在进行垃圾收集时可以根据用户设定的可接受停顿时间来进行分析,找出在设定的时间范围内收集哪些区域最划算,然后优先收集这些区域。这样做不仅可以优先收集垃圾最多的Region
,还可以根据用户的设定来计算收集哪些Region
可以达到用户所期望的垃圾收集时间。- 通过 CSet,G1 垃圾收集器的性能得到了极大的提升,并且能够实现可预测的停顿时间要求。这使得垃圾回收过程变得更加智能化,更加适应不同的应用场景和用户需求。需要注意的是,用户设定的时间应该合理,官方建议在 100ms 到 300ms 之间,以平衡垃圾收集的效率和停顿时间的需求。
3.特点
- 年轻代和老年代是各自独立且连续的内存块。
- 年轻代收集使用伊甸园区+幸存0区+幸存1区进行复制算法。
- 老年代收集必须扫描整个老年代区域。
- 都是以尽可能少而快速地执行GC为设计原则。
4.与CMS相比
- G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
- 并行与并发:
G1
能充分利用CPU、多核环境下的硬件优势,使用多个CPU
(CPU
或者CPU
核心)来缩短stop-The-World
停顿时间。部分其他收集器原本需要停顿Java
线程执行的GC
动作,G1
收集器仍然可以通过并发的方式让java
程序继续执行。 - 分代收集:分代概念在
G1
中依然得以保留。虽然G1
可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC
的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。 - 空间整合:由于
G1
使用了独立区域(Region
)概念,单个Region大小=堆总大小/2048=2M
,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region
)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。 - 可预测的停顿:这是
G1
相对于CMS
的另一大优势,降低停顿时间是G1
和CMS
共同的关注点,但G1
除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
- 并行与并发:
- 上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。
5.Region区域
- 使用
G1
收集器时,Java
堆的内存布局与其他收集器有很大差别,它将整个Java
堆划分为多个大小相等的独立区域(Region
),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region
的集合。
- 每个
Region
被标记了E
、S
、O
和H
,说明每个Region
在运行时都充当了一种角色,其中H
是以往算法中没有的,它代表Humongous
,这表示这些Region
存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region
大小一半时,直接在新的一个或多个连续Region
中分配,并标记为H
。
6.收集流程
G1
垃圾收集器的回收流程与CMS
的逻辑大致相同,包括初始标记、并发标记、重新标记和筛选清除等阶段。但是,与CMS
不同的是,G1
在最后一个阶段不会直接进行整体的清除。相反,它会根据用户设置的停顿时间进行智能的筛选和局部的回收。通过这种智能的筛选和局部回收方式,G1 垃圾收集器能够更好地平衡垃圾回收的效率和停顿时间,从而提高系统的响应性和用户体验。- 初始标记(
Initial Mark
):在初始标记阶段,G1
垃圾收集器会对根对象进行一次快速的标记,标记出所有与根对象直接关联的存活对象。这个阶段需要暂停用户线程,以确保标记的准确性。 - 并发标记(
Concurrent Mark
):在并发标记阶段,G1
垃圾收集器会与用户线程并发执行,对整个堆进行标记。垃圾回收线程会在后台标记所有存活对象,而用户线程可以继续执行,不受影响。 - 重新标记(
Remark
):在并发标记阶段结束后,G1
垃圾收集器会进行一次重新标记,来处理在并发标记阶段发生变化的对象。这个阶段需要暂停用户线程,以确保标记的准确性。 - 筛选清除(
Concurrent Cleanup
):在重新标记完成后,G1
垃圾收集器不会立即进行整体的清除操作。相反,它会根据用户设置的停顿时间智能地筛选出需要回收的Region
,并执行局部的回收。这样可以在尽量满足停顿时间的情况下,最大限度地回收垃圾。
- 初始标记(
7.示例代码
//-Xms10m -Xmx10m -Xlog:gc*
public class OOM_G1Demo{
public static String baseString = "www.atguigu.com";
public static void main(String[] args){
List<String> list = new ArrayList<>();
for (int i = 1; i <=10000 ; i++) {
String tmpString = baseString + baseString;
baseString = tmpString;
list.add(tmpString);
}
}
}
8.补充
- 针对
Eden
区进行收集,Eden
区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内存碎片将存活的对象(即复制或移动)到一或多个幸存者区域,如满足老化阈值则某些对象将被提升到老年代。Eden
区的数据移动到Survivor
区,假如出现Survivor
区空间不够,Eden
区数据会部分晋升到Old
区。Survivor
区的数据移动到新的Survivor
区,如果满足老化阈值,则某些对象将被晋升到Old
区。- 最后
Eden
区收拾干净了,GC
结束,用户的应用程序继续执行。
- 注:
- 收集区域: 整个堆内存
- 使用算法: 标记复制法
- 搜集方式: 多线程
4.7.6、ZGC收集器
1.介绍
ZGC(Z Garbage Collector)
是一种低延迟的垃圾回收器,是JDK 11
引入的一项重要特性。ZGC
的出现为Java
应用提供了一种更加高效、可预测的垃圾回收解决方案,与传统的垃圾回收器相比,ZGC
的主要目标是实现极低的垃圾回收停顿时间,使得 Java
应用能够以更可预测的方式运行,尤其在大内存堆上表现良好。
2.特点
- 低停顿时间:
ZGC
致力于将垃圾回收的停顿时间降至最低。它通过并发标记、并发清理等技术,在整个垃圾回收过程中尽量减少对应用程序的影响,从而实现了极低的垃圾回收停顿时间。这使得 Java 应用能够更加平滑地运行,减少了因垃圾回收而导致的不可预测性和性能波动。 - 可预测性:
ZGC
的设计注重可预测性,即使在大内存堆上,也能够提供稳定的性能和可预测的垃圾回收行为。这使得开发人员能够更加信任和依赖于 Java 应用在生产环境中的稳定性和可靠性。 - 适用于大内存堆:
ZGC
的低停顿时间特性使其特别适用于大内存堆的场景。在这种场景下,传统的垃圾回收器可能会面临长时间的停顿,影响应用的响应性和用户体验,而ZGC
能够有效地缓解这一问题,保持较低的停顿时间,从而确保应用的流畅运行。
3.垃圾收集流程
ZGC
的垃圾回收过程几乎全部都是并发执行的,即与应用程序线程同时进行。- 初始标记(
Initial Mark
):在初始标记阶段,ZGC
会标记出根对象以及直接与根对象关联的存活对象。这个阶段需要短暂地暂停所有应用线程,以确保标记的准确性。 - 并发标记(
Concurrent Mark
):在并发标记阶段,ZGC
与应用程序线程并发执行,标记所有存活对象。这个阶段不会暂停应用程序线程,因此垃圾回收和应用程序可以并发执行。 - 最终标记(
Final Mark
):在并发标记阶段结束后,ZGC
需要再次短暂地暂停所有应用线程,完成最终的标记工作。这个阶段主要用于标记在并发标记阶段有可能发生变化的对象。 - 筛选(
Concurrent Sweep
):在最终标记完成后,ZGC
会进行一次筛选,确定哪些对象可以被回收。这个阶段会并发地进行,不会暂停应用程序线程。并发清除(Concurrent Cleanup
):在筛选阶段完成后,ZGC
会并发地清除未被标记的对象,释放它们所占用的内存。这个阶段也不会暂停应用程序线程。
- 初始标记(
4.补充
- 注:
- 收集区域: 整个堆内存
- 使用算法: 并发标记法
- 搜集方式: 多线程
4.8、垃圾回收器补充知识
4.8.1、主流垃圾回收器
- 新生代垃圾收集器:
Serial
、Parallel Scavenge
、ParNew
- 老年代垃圾收集器:
Serial Old
、Parallel Old
、CMS
- 整理收集器:
G1
4.8.2、垃圾回收器组合关系
如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
JDK8
中默认使用组合是:Parallel Scavenge GC
、ParallelOld GC
。JDK9
开始及之后默认是用G1
为垃圾收集器JDK14
弃用了:Parallel Scavenge GC
、Parallel OldGC
。JDK14
移除了CMS GC
4.8.3、GC性能指标
名称 | 说明 |
---|---|
吞吐量 | 即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99% |
暂停时间 | 执行垃圾回收时,程序的工作线程被暂停的时间 |
内存占用 | java堆所占内存的大小 |
收集频率 | 垃圾收集的频次 |
4.8.4、垃圾回收器选择策略
- 吞吐率优先的服务端程序(比如计算密集型) :
Parallel Scavenge + Parallel Old
。 - 响应时间优先的服务端程序 :
ParNew + CMS
。 G1
收集器是基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First
)。- 目前主流的垃圾回收器配置是新生代采用
ParNew
,老年代采用CMS
组合的方式,或者是完全采用G1回收器,从未来的趋势来看,G1是官方维护和更为推崇的垃圾回收器。
4.9、垃圾回收流程
- 新生代垃圾回收流程(
Minor GC
):- 新创建的对象首先分配在
Eden
区(除非配置了-XX:PretenureSizeThreshold
,大于该值的对象会直接进入年老代)。 Eden
区满时触发新生代垃圾回收(Minor GC
)。- 当
Eden
区满了或放不下了,这时候其中存活的对象会复制到from
区(这里,需要注意的是,如果存活下来的对象from区都放不下,则这些存活下来的对象全部进入年老代。之后Eden区的内存全部回收掉)。 - 之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和from区存活下来的对象复制到to区(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和from区的所有内存。
- 存活对象的年龄(每复制一次,对象的年龄就+1)加 1。
- 清空
Eden
区和From Survivor
区。 - 交换
From
和To Survivor
区的角色。 - 当对象的年龄达到一定阈值(默认
15
,这个次数可以通过-XX:MaxTenuringThreshold
来配置)会被晋升到老年代。
- 新创建的对象首先分配在
- 老年代垃圾回收过程(
Major GC/Full GC
):- 当对象的年龄达到阈值,或
Survivor
区空间不足时,对象会被晋升到老年代。 - 当老年代空间不足时,触发老年代垃圾回收(
Major GC
)或Full GC
(这个是我们最需要减少的,因为耗时很严重)。
- 当对象的年龄达到阈值,或
- 永久代/元空间(
PermGen/Metaspace
)垃圾回收:永久代/元空间的垃圾回收主要针对常量池和类卸载,当类不再被引用时,其元数据会被回收。
4.10、垃圾回收区域
Minor GC(新生代垃圾回收)
:回收新生代(Young Generation
)中的垃圾对象。- 触发条件:当 Eden 区满时触发。
- 特点:
- 回收速度快,停顿时间短。
- 使用复制算法,将存活的对象从
Eden
区和From Survivor
区复制到To Survivor
区。 - 存活对象的年龄(Age)加 1,当年龄达到阈值(默认 15)时,对象会被晋升到老年代。
Major GC(老年代垃圾回收)
:回收老年代(Old Generation
)中的垃圾对象。- 触发条件:当老年代空间不足时触发。
- 特点:
- 回收速度较慢,停顿时间较长。
- 使用标记-清除算法或标记-整理算法。
- 通常与
Full GC
同时发生。
Full GC(全局垃圾回收)
:回收整个堆内存(新生代和老年代)以及方法区(Metaspace
)中的垃圾对象。- 触发条件:
- 老年代空间不足。
- 方法区空间不足。
- 调用
System.gc()
(不保证立即执行)。
- 特点:
- 回收速度最慢,停顿时间最长。
- 对整个堆内存和方法区进行回收。
- 触发条件:
五、线上问题定位
不同的问题【接口报错,RT超时、CPU飙高、OOM…】排查方案是不一样的。
5.1、CPU飙升问题排查
5.1.1、示例代码
package com.atguigu.study.jvm;
import java.util.UUID;
//放入Linux系统或者阿里云服务器,运行后故意让cpu飙高
public class HighCPUDemo
{
public static void main(String[] args)
{
while (true)
{
System.out.println("--------hello atguigu"+"\t"+ UUID.randomUUID().toString());
}
}
}
5.1.2、排查流程
- 使用top命令找到cpu飙升进程id。
- 根据进程id找到导致cpu飙升的线程
ps H -eo pid,tid,%cpu | grep 进程id
- 将线程id转换为16进制
printf '0x%x\n' 线程id
- 根据线程定位问题代码
jstack 进程id | grep 16进制线程id -A 20
解释:
jstack:jdk内置命令,用于查看某个java进程所有线程快照,里面包含了线程详细的堆栈信息
grep:从大量文本中快速找到某个关键字所在的行,-A参数后面的20,表示找到内容后,取内容所在行后面20行记录
5.2、Arthas
– 阿尔萨斯
5.2.1、介绍
Arthas
:阿里开源的一款Java问题诊断利器,
详情见:Arthas官网
5.2.1、操作流程
- 启动阿尔萨斯命令:
java -jar arthas-boot.jar
- 按照提示输入数字进入阿尔萨斯
- 进arthas后,用thread命令查看cpu占比最高的线程
- 使用thread 线程id查看线程堆栈,定位问题代码。
六、JVM指令
6.1、指令
jps(JVM Process Status Tool)
:显示指定系统内所有的HotSpot虚拟机进程。jstat(JVM Statistics Monitoring Tool)
:用户收集HotSpot
虚拟机各方面的运行数据。jinfo(Configuration Info for Java)
:实时查看和调整虚拟机各项参数,在JDK9
中集成到了jhsdb
。jmap(Memory Map for java)
:生成虚拟机的内存转储快照,在JDK9
中集成到了jhsdb
。jhat(JVM Heap Dump Browser)
:用户分析heapdump
文件,它会建立一个。HTTP/HTML
服务器,让用户可以在浏览器上查看分析结果,在JDK9
中集成到了jhsdb
。jstack(Stack Trace for Java)
:生成虚拟机当前时刻的线程快照,在JDK9
中集成到了jhsdb。jhsdb(Java HotSport Debugger)
:一个基于Serviceability Agent
的HotSpot
进程调试器,在JDK9
引入。jsadebugd(Java Serviceability Agent Debug Daemon)
:适用于java
的可维护代理调试守护程序,主要用于附加指定的Java
进程,核心文件,或充当一个调试服务器。jcmd(JVM Command)
:虚拟机诊断命令工具,将诊断命令请求发送到正在运行的Java虚拟机。从JDK7
开始提供。jconsole(Java Console)
:用于监控Java虚拟机的使用JMX
规范的图形工具。它可以监控本地和远程Java
虚拟机,还可以监控和管理应用程序。jmc(Java Mission Control)
:包含用于监控和管理Java
应用程序的工具,而不会引入与这些工具相关联的性能开销。开发者可以使用jmc
命令来创建JMC
工具,从JDK7 Update 40
开始集成到OracleJDK
中。jvisualvm(Java VisualVM)
:一种图形化工具,可在Java虚拟机中运行时提供有关基于Java
技术的应用程序(Java
应用程序)的详细信息,Java VisualVM
提供内存和CPU
分析、堆转储分析、内存泄漏检测、MBean
访问和垃圾收集。从JDK 6 Update 7
开始提供;从JDK 9
开始不再打包如JDK
中,但仍保持更新发展,可独立下载。
6.2、指令详解
1、jps(JVM Process Status Tool)
指令
jps [option] [hostid]
option参数:
-p: 仅仅显示VM 标示,不显示jar,class, main参数等信息.
-m: 输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整package名称或jar完整名称.
-v: 列出jvm参数
2、jstat(Memory Map for java)
指令
是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT
编译等运行数据,在没有GUI
图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
jstat -<option> <vmid> [<interval> [<count>]]
<option>:指定要监控的统计信息类型(如 -gc、-gcutil 等)。
<vmid>:虚拟机进程 ID(PID)。
<interval>:输出间隔时间(以毫秒为单位)。
<count>:输出次数。
- option参数:
- 结果含义
缩写 | 含义 |
---|---|
S0C(Survivor 0 Capacity (KB)) | 第一个Survivor 区(S0 )的当前容量(以 KB 为单位)。 |
S1C(Survivor 1 Capacity (KB)) | 第二个Survivor 区(S1 )的当前容量(以 KB 为单位)。 |
S0U(Survivor 0 Used (KB)) | 第一个Survivor 区(S0 )的已使用容量(以 KB 为单位)。 |
S1U Survivor 1 Used (KB) | 第二个Survivor 区(S1 )的已使用容量(以 KB 为单位)。 |
EC Eden Capacity (KB) | Eden 区的当前容量(以KB为单位)。 |
EU Eden Used (KB) | Eden 区的已使用容量(以KB为单位)。 |
OC Old Capacity (KB) | 老年代(Old Generation )的当前容量(以KB为单位)。 |
OU Old Used (KB) | 老年代(Old Generation )的已使用容量(以KB为单位)。 |
MC Metaspace Capacity (KB) | 元空间(Metaspace )的当前容量(以 KB 为单位)。 |
MU Metaspace Used (KB) | 元空间(Metaspace )的已使用容量(以 KB 为单位)。 |
CCSC Compressed Class Space Capacity (KB) | 压缩类空间(Compressed Class Space )的当前容量(以 KB 为单位)。 |
CCSU Compressed Class Space Used (KB) | 压缩类空间(Compressed Class Space )的已使用容量(以 KB 为单位)。 |
YGC Young Generation Collections | 新生代垃圾回收的次数。 |
YGCT Young Generation Collection Time (s) | 新生代垃圾回收的总时间(以秒为单位)。 |
FGC Full GC Collections | Full GC (全局垃圾回收)的次数。 |
FGCT Full GC Collection Time (s) | Full GC (全局垃圾回收)的总时间(以秒为单位)。 |
CGC(Concurrent GC Count) | CGC 表示 并发垃圾回收的次数。它记录了并发垃圾回收器(如 G1 或CMS )在并发阶段执行的次数。 |
CGCT(Concurrent GC Time) | CGCT 表示 并发垃圾回收的总时间(以秒为单位)。它记录了并发垃圾回收器在并发阶段花费的总时间。 |
GCT Total GC Collection Time (s) | 所有垃圾回收的总时间(包括YGCT 和FGCT ,以秒为单位)。 |
TT(Tenuring Threshold) | 对象晋升到老年代的年龄阈值。 |
MTT (Maximum Tenuring Threshold) | 对象晋升到老年代的最大年龄阈值。 |
DSS(Desired Survivor Size (KB)) | Survivor 区的期望大小(以 KB 为单位)。 |
CCSMN Compressed Class Space Min (KB) | 压缩类空间的最小容量(以 KB 为单位)。 |
CCSMX Compressed Class Space Max (KB) | 压缩类空间的最大容量(以 KB 为单位)。 |
MCMN Metaspace Capacity Min (KB) | 元空间的最小容量(以 KB 为单位)。 |
MCMX Metaspace Capacity Max (KB) | 元空间的最大容量(以 KB 为单位)。 |
OGCMN Old Generation Capacity Min (KB) | 老年代的最小容量(以 KB 为单位)。 |
OGCMX Old Generation Capacity Max (KB) | 老年代的最大容量(以 KB 为单位)。 |
OGC Old Generation Capacity (KB) | 老年代的当前容量(以 KB 为单位)。 |
NGCMN New Generation Capacity Min (KB) | 新生代的最小容量(以 KB 为单位)。 |
NGCMX New Generation Capacity Max (KB) | 新生代的最大容量(以 KB 为单位)。 |
NGC New Generation Capacity (KB) | 新生代的当前容量(以 KB 为单位)。 |
E Eden Utilization (%) | Eden 区的使用百分比。 |
O Old Utilization (%) | 老年代(Old Generation )的使用百分比。 |
M Metaspace Utilization (%) | 元空间(Metaspace )的使用百分比。 |
CCS Compressed Class Space Utilization (%) | 压缩类空间(Compressed Class Space )的使用百分比。 |
3、jinfo(Configuration Info for Java)
指令
jinfo [option] pid
option参数:
–sysprops 可以查看由System.getProperties()取得的参数
–flag 未被显式指定的参数的系统默认值
–flags(注意s)显示虚拟机的参数
–flag +[参数] 可以增加参数,但是仅限于由java -XX:+PrintFlagsFinal –version查询出来且
4、jmap(Memory Map for java)
指令
用于生成堆转储快照(一般称为heapdump
或dump
文件)。jmap
的作用并不仅仅是为了获取dump
文件,它还可以查询finalize
执行队列、Java
堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。和jinfo
命令一样,jmap有不少功能在Windows
平台下都是受限的,除了生成dump
文件的-dump
选项和用于查看每个类的实例、空间占用统计的-histo
选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris
下使用。
jmp [option] pid
option
参数
输出文件的内容:num
:序号instances
:实例数量bytes
:占用空间大小class name
:类名称,[C
is achar[]
,[S
is ashort[]
,[I
is aint[]
,[B
is abyte[]
,[[I
is aint[][]
5、jhat(JVM Heap Dump Browser)
指令
屏幕显示"Server is ready
"的提示后,用户在浏览器中输入http://localhost:7000/
可以看到分析结果,拖到最下面,主要看"HeapHistogram
"。
6、jstack(Stack Trace for Java)
指令
jstack(Stack Trace for Java)
命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。- 在代码中可以用
java.lang.Thread
类的getAllStackTraces()
方法用于获取虚拟机中所有线程的StackTraceElement
对象。使用这个方法可以通过简单的几行代码就完成jstack
的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
jstack [option] pid
option参数:
-F:当正常输出的请求不被响应时,强制输出线程堆栈
-l:除堆栈外,显示关于锁的附加信息
-m:如果调用到本地方法的话,可以显示C/C++的堆栈
7、jhsdb(Java HotSport Debugger)
指令
jhsdb
是JDK 9
引入的一个调试工具,用于分析JVM
的运行状态。它可以调试正在运行的JVM
进程或核心转储文件,提供堆分析、线程分析、类加载器分析等功能。
jhsdb <command> <pid>
<command>:要执行的命令(如 jstack、jmap 等)。
<pid>:目标 JVM 进程的 PID。
8、jsadebugd(Java Serviceability Agent Debug Daemon)
指令
jsadebugd
是一个调试守护进程,用于远程调试JVM
进程。
jsadebugd <pid> [<server-id>]
<pid>:目标 JVM 进程的 PID。
<server-id>:可选的服务器标识符。
9、jcmd(JVM Command)
指令
jcmd
是一个多功能命令行工具,用于向JVM
进程发送诊断命令。它可以获取JVM
的运行状态、生成堆转储文件、触发垃圾回收等。
- 列出所有 JVM 进程:
jcmd -l
- 查看 JVM 的运行状态:
jcmd <pid> VM.version
- 生成堆转储文件:
jcmd <pid> GC.heap_dump <file-path>
- 触发垃圾回收:
jcmd <pid> GC.run
10、jconsole(Java Console)
指令
jconsole
是一个基于 GUI 的监控工具,用于实时监控 JVM 的运行状态,包括内存使用、线程状态、类加载情况等。
jconsole <pid>
11、jmc(Java Mission Control)
指令
使用同上
12、jvisualvm(Java VisualVM)
指令
使用同上