JVM 深入
Java 虚拟机核心原理、内存管理、GC 调优与故障排查。
1. JVM 架构概述
1.1 什么是 JVM
JVM(Java Virtual Machine)是 Java 实现"一次编写,到处运行"的基石。它是一个抽象的计算机,有自己完善的指令集、寄存器、栈、堆和方法区。JVM 屏蔽了底层操作系统的差异,使得 Java 字节码可以在任何安装了 JVM 的平台上执行。
1.2 JVM 整体架构
JVM 主要由以下几大子系统组成:
Java 源代码 (.java)
|
v javac 编译
Java 字节码 (.class)
|
v
+-----------------------+
| 类加载子系统 | 将 .class 文件加载到运行时数据区
+-----------------------+
|
v
+-----------------------+
| 运行时数据区 | 内存区域划分
| +--------+----------+|
| | 堆 | 方法区 ||
| +--------+----------+|
| | 虚拟机栈 | 本地方法栈||
| +--------+----------+|
| | 程序计数器 ||
| +-------------------+|
+-----------------------+
|
v
+-----------------------+
| 执行引擎 | 解释/编译执行字节码
| +--------+----------+|
| | 解释器 | JIT编译器||
| +--------+----------+|
| | 垃圾回收器 ||
| +-------------------+|
+-----------------------+
|
v
+-----------------------+
| 本地方法接口 (JNI) | 调用 C/C++ 本地方法
+-----------------------+Java 源代码 (.java)
|
v javac 编译
Java 字节码 (.class)
|
v
+-----------------------+
| 类加载子系统 | 将 .class 文件加载到运行时数据区
+-----------------------+
|
v
+-----------------------+
| 运行时数据区 | 内存区域划分
| +--------+----------+|
| | 堆 | 方法区 ||
| +--------+----------+|
| | 虚拟机栈 | 本地方法栈||
| +--------+----------+|
| | 程序计数器 ||
| +-------------------+|
+-----------------------+
|
v
+-----------------------+
| 执行引擎 | 解释/编译执行字节码
| +--------+----------+|
| | 解释器 | JIT编译器||
| +--------+----------+|
| | 垃圾回收器 ||
| +-------------------+|
+-----------------------+
|
v
+-----------------------+
| 本地方法接口 (JNI) | 调用 C/C++ 本地方法
+-----------------------+1.3 HotSpot VM
HotSpot 是 Oracle JDK / OpenJDK 的默认虚拟机,也是目前使用最广泛的 JVM。名称源自其具备"热点代码探测"能力:
- 热点探测:通过执行计数器找出频繁执行的代码(热点代码),将其编译为本地机器码。
- 即时编译(JIT):C1(Client Compiler)和 C2(Server Compiler)两级编译器。
- 分代收集:成熟的垃圾回收体系,支持从 Serial 到 ZGC 的多种回收器。
HotSpot 的主要版本演进:
| JDK 版本 | 主要 JVM 特性 |
|---|---|
| JDK 8 | 永久代 -> 元空间,Lambda 支持 |
| JDK 9 | G1 成为默认 GC,统一 GC 日志 |
| JDK 11 | ZGC 实验性支持,Epsilon GC |
| JDK 12 | Shenandoah GC |
| JDK 14 | ZGC 正式支持(macOS/Windows) |
| JDK 15 | ZGC 正式产品化 |
| JDK 17 | ZGC 分代支持(实验性) |
| JDK 21 | 分代 ZGC 正式版,虚拟线程 |
2. 类加载机制(重点)
2.1 类加载过程总览
一个类从被加载到虚拟机内存中,到卸载出内存,整个生命周期包括七个阶段:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
|______ 链接(Linking)_______| |_ 初始化 _|加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
|______ 链接(Linking)_______| |_ 初始化 _|前三阶段(加载、验证、准备)是按顺序开始的,而解析阶段则不一定:为了支持动态绑定,解析可以在初始化之后进行。
2.2 加载(Loading)
加载阶段完成三件事:
- 通过类的全限定名获取定义该类的二进制字节流。
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成代表该类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
// 触发类加载的几种方式
public class ClassLoadingDemo {
public static void main(String[] args) throws Exception {
// 1. new 关键字
MyClass obj = new MyClass();
// 2. Class.forName() - 会执行初始化
Class<?> clazz1 = Class.forName("com.example.MyClass");
// 3. ClassLoader.loadClass() - 不会执行初始化
Class<?> clazz2 = ClassLoader.getSystemClassLoader()
.loadClass("com.example.MyClass");
// 4. 访问静态字段/方法
int val = MyClass.STATIC_FIELD;
// 5. 反射调用
Class<?> clazz3 = MyClass.class;
}
}// 触发类加载的几种方式
public class ClassLoadingDemo {
public static void main(String[] args) throws Exception {
// 1. new 关键字
MyClass obj = new MyClass();
// 2. Class.forName() - 会执行初始化
Class<?> clazz1 = Class.forName("com.example.MyClass");
// 3. ClassLoader.loadClass() - 不会执行初始化
Class<?> clazz2 = ClassLoader.getSystemClassLoader()
.loadClass("com.example.MyClass");
// 4. 访问静态字段/方法
int val = MyClass.STATIC_FIELD;
// 5. 反射调用
Class<?> clazz3 = MyClass.class;
}
}2.3 链接(Linking)
验证(Verification)
确保 Class 文件的字节流包含的信息符合规范,不会危害虚拟机自身安全。包括:
- 文件格式验证:魔数 0xCAFEBABE、版本号等。
- 元数据验证:语义检查(是否有父类、是否继承了 final 类等)。
- 字节码验证:通过数据流和控制流分析,确保语义合法。
- 符号引用验证:对常量池中的符号引用进行匹配性检查。
准备(Preparation)
为类变量(static 修饰的变量)分配内存并设置初始值。注意:
- 仅包含类变量(static),不包含实例变量。
- 初始值通常是零值,而非代码中赋的值。
public class PrepareDemo {
// 准备阶段后 value = 0,而非 123
// 初始化阶段才将 value 赋值为 123
public static int value = 123;
// 对于 final static 常量,准备阶段直接赋值为常量值
public static final String CONST = "hello";
// 引用类型准备阶段为 null
public static Object obj = new Object();
}public class PrepareDemo {
// 准备阶段后 value = 0,而非 123
// 初始化阶段才将 value 赋值为 123
public static int value = 123;
// 对于 final static 常量,准备阶段直接赋值为常量值
public static final String CONST = "hello";
// 引用类型准备阶段为 null
public static Object obj = new Object();
}解析(Resolution)
将常量池中的符号引用替换为直接引用的过程。
- 符号引用:用字面量描述目标,与内存布局无关。
- 直接引用:直接指向目标的指针、相对偏移量或句柄。
解析时机:虚拟机实现可以自行决定,HotSpot 采用延迟解析,在指令真正使用到符号引用时才解析。
2.4 初始化(Initialization)
执行类构造器 <clinit>() 方法的过程。<clinit>() 由编译器自动收集类中所有静态变量的赋值动作和静态语句块合并产生。
public class InitDemo {
// 静态变量
public static int a = 10;
// 静态代码块
static {
a = 20;
b = 30; // 可以赋值,但不能访问(非法前向引用)
}
public static int b = 40; // 注意此处会覆盖 static 块中 b 的赋值
public static void main(String[] args) {
System.out.println(a); // 20
System.out.println(b); // 40
}
}public class InitDemo {
// 静态变量
public static int a = 10;
// 静态代码块
static {
a = 20;
b = 30; // 可以赋值,但不能访问(非法前向引用)
}
public static int b = 40; // 注意此处会覆盖 static 块中 b 的赋值
public static void main(String[] args) {
System.out.println(a); // 20
System.out.println(b); // 40
}
}<clinit>() 执行规则:
- 线程安全:虚拟机会保证多个线程同时初始化一个类时,只有一个线程执行
<clinit>()。 - 父类先于子类:父类的
<clinit>()先执行。 - 接口例外:接口初始化不要求父接口先初始化,除非用到父接口中的变量。
- 可选:如果没有静态变量和静态代码块,编译器不会生成
<clinit>()。
不会触发初始化的场景(被动引用):
public class PassiveRefDemo {
public static void main(String[] args) {
// 1. 通过子类引用父类的静态字段,不会触发子类初始化
int x = Child.value; // 只触发 Parent 初始化
// 2. 通过数组定义引用类,不会触发初始化
Parent[] arr = new Parent[10];
// 3. 访问常量(编译期存入常量池),不会触发初始化
System.out.println(Child.CONST_VAL);
}
}public class PassiveRefDemo {
public static void main(String[] args) {
// 1. 通过子类引用父类的静态字段,不会触发子类初始化
int x = Child.value; // 只触发 Parent 初始化
// 2. 通过数组定义引用类,不会触发初始化
Parent[] arr = new Parent[10];
// 3. 访问常量(编译期存入常量池),不会触发初始化
System.out.println(Child.CONST_VAL);
}
}2.5 类加载器
类加载器负责通过一个类的全限定名来获取描述该类的二进制字节流。
2.5.1 类加载器层次结构
Bootstrap ClassLoader(启动类加载器)
| C++ 实现,是 JVM 的一部分
| 加载 <JAVA_HOME>/lib/ 目录下的核心库(rt.jar、charsets.jar 等)
v
Platform ClassLoader(平台类加载器,JDK 9+)
| Java 9 之前称为 Extension ClassLoader
| 加载 <JAVA_HOME>/lib/ext/ 目录或 java.ext.dirs 指定的类
v
Application ClassLoader(应用程序类加载器)
| 加载 classpath(-classpath 或 -cp)下指定的类
| 程序中默认的类加载器
v
自定义 ClassLoader(用户自定义)Bootstrap ClassLoader(启动类加载器)
| C++ 实现,是 JVM 的一部分
| 加载 <JAVA_HOME>/lib/ 目录下的核心库(rt.jar、charsets.jar 等)
v
Platform ClassLoader(平台类加载器,JDK 9+)
| Java 9 之前称为 Extension ClassLoader
| 加载 <JAVA_HOME>/lib/ext/ 目录或 java.ext.dirs 指定的类
v
Application ClassLoader(应用程序类加载器)
| 加载 classpath(-classpath 或 -cp)下指定的类
| 程序中默认的类加载器
v
自定义 ClassLoader(用户自定义)JDK 9 模块化系统对类加载器进行了调整:
- Boot ClassLoader:仍为最顶层,加载
java.base等基础模块。 - Platform ClassLoader:替代 Extension ClassLoader,加载除基础模块外的其他 JDK 模块。
- Application ClassLoader:负责加载模块路径和类路径上的类。
2.5.2 双亲委派模型
当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把请求委派给父加载器去完成,每一层如此,最终所有加载请求都会传到启动类加载器。只有当父加载器反馈自己无法完成加载时,子加载器才会尝试自己加载。
// 双亲委派的核心逻辑(java.lang.ClassLoader.loadClass() 简化版)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 第一步:检查是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 第二步:父加载器未加载过,委托给父加载器
c = parent.loadClass(name, false);
} else {
// 第三步:没有父加载器,委托给 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法完成加载
}
if (c == null) {
// 第四步:父加载器加载失败,自己尝试加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}// 双亲委派的核心逻辑(java.lang.ClassLoader.loadClass() 简化版)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 第一步:检查是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 第二步:父加载器未加载过,委托给父加载器
c = parent.loadClass(name, false);
} else {
// 第三步:没有父加载器,委托给 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法完成加载
}
if (c == null) {
// 第四步:父加载器加载失败,自己尝试加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}为什么需要双亲委派:
- 安全性:防止核心 API 被篡改。例如自己写一个
java.lang.String类,双亲委派机制会让 Bootstrap ClassLoader 优先加载 JDK 自带的 String,确保核心类不被替换。 - 唯一性:保证同一个类在全加载器层次中具有唯一性。如果两个类加载器各自加载了同一个类,在 JVM 中会被视为两个不同的类。
2.5.3 如何打破双亲委派
方式一:重写 loadClass 方法
// 自定义类加载器,打破双亲委派
public class BreakParentDelegationClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 先检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 对于 JDK 核心类,仍用 Bootstrap ClassLoader
if (name.startsWith("java.")) {
c = getParent().loadClass(name);
} else {
// 对于非核心类,自己加载
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从指定路径加载 .class 文件的字节码
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
// 实现从特定路径读取 .class 文件
// ...
return null;
}
}// 自定义类加载器,打破双亲委派
public class BreakParentDelegationClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 先检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 对于 JDK 核心类,仍用 Bootstrap ClassLoader
if (name.startsWith("java.")) {
c = getParent().loadClass(name);
} else {
// 对于非核心类,自己加载
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从指定路径加载 .class 文件的字节码
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
// 实现从特定路径读取 .class 文件
// ...
return null;
}
}实际场景:
| 场景 | 实现 | 说明 |
|---|---|---|
| Tomcat | 每个 WebApp 有自己的 ClassLoader | 隔离不同应用的类,优先加载 WebApp 自己的类 |
| OSGi | 网状类加载模型 | 每个 Bundle 有自己的 ClassLoader,实现模块化热部署 |
| SPI | 线程上下文类加载器 | 父加载器加载的代码需要访问子加载器的类 |
2.5.4 线程上下文类加载器
Java 的 SPI(Service Provider Interface)如 JDBC、JNDI 等,通常由 Bootstrap ClassLoader 加载核心接口,但具体实现由第三方提供(在 classpath 下)。这产生了"父加载器要访问子加载器加载的类"的矛盾。
解决方案:线程上下文类加载器(Thread Context ClassLoader)。
// JDBC 驱动加载示例(DriverManager 由 Bootstrap ClassLoader 加载)
public class DriverManager {
static {
loadInitialDrivers();
}
private static void loadInitialDrivers() {
// 使用线程上下文类加载器加载第三方驱动实现
Thread.currentThread().getContextClassLoader();
// ServiceLoader 会使用 TCCL 加载 META-INF/services/ 下的实现
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
}
}// JDBC 驱动加载示例(DriverManager 由 Bootstrap ClassLoader 加载)
public class DriverManager {
static {
loadInitialDrivers();
}
private static void loadInitialDrivers() {
// 使用线程上下文类加载器加载第三方驱动实现
Thread.currentThread().getContextClassLoader();
// ServiceLoader 会使用 TCCL 加载 META-INF/services/ 下的实现
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
}
}2.6 模块化类加载(Java 9+)
Java 9 引入模块化系统(Project Jigsaw)后,类加载机制的变化:
- 类加载器层次从双亲委派变为模块委派。
- 对于可以明确归属到某个模块的类,优先从该模块加载。
- 对于未命名模块(传统 classpath 中的类),仍然遵循双亲委派。
3. 运行时数据区(重点)
JVM 在执行 Java 程序过程中会将其管理的内存划分为不同的数据区域。
+-------------------------------------------------------+
| 运行时数据区 |
| |
| +---------------------------+------------------------+ |
| | 堆 (Heap) | 元空间 | |
| | - 新生代 | (Metaspace, JDK 8+) | |
| | - Eden | - 类元数据 | |
| | - Survivor (S0, S1) | - 常量池 | |
| | - 老年代 | - JIT 编译代码 | |
| +---------------------------+------------------------+ |
| +--------------------------------------------------+ |
| | 虚拟机栈 (每个线程一个) | |
| | - 栈帧 1: 局部变量表 + 操作数栈 + 动态链接 + 返回 | |
| | - 栈帧 2: ... | |
| +--------------------------------------------------+ |
| +------------------+ +---------------------------+ |
| | 程序计数器 (PC) | | 本地方法栈 | |
| | (每个线程一个) | | (每个线程一个) | |
| +------------------+ +---------------------------+ |
+-------------------------------------------------------++-------------------------------------------------------+
| 运行时数据区 |
| |
| +---------------------------+------------------------+ |
| | 堆 (Heap) | 元空间 | |
| | - 新生代 | (Metaspace, JDK 8+) | |
| | - Eden | - 类元数据 | |
| | - Survivor (S0, S1) | - 常量池 | |
| | - 老年代 | - JIT 编译代码 | |
| +---------------------------+------------------------+ |
| +--------------------------------------------------+ |
| | 虚拟机栈 (每个线程一个) | |
| | - 栈帧 1: 局部变量表 + 操作数栈 + 动态链接 + 返回 | |
| | - 栈帧 2: ... | |
| +--------------------------------------------------+ |
| +------------------+ +---------------------------+ |
| | 程序计数器 (PC) | | 本地方法栈 | |
| | (每个线程一个) | | (每个线程一个) | |
| +------------------+ +---------------------------+ |
+-------------------------------------------------------+3.1 堆(Heap)
堆是 JVM 管理的最大一块内存区域,所有线程共享,在虚拟机启动时创建。堆的唯一目的是存放对象实例(几乎所有的对象都在这里分配)。
3.1.1 堆内存结构(分代布局)
+-------------------------------------------------------+
| 堆 (Heap) |
| |
| +-------------------+--------------------+--------+ |
| | 新生代 | | | |
| | (Young Gen) | 老年代 | 元空间 | |
| | | (Old Gen) | | |
| | +------+---+---+ | | (本地 | |
| | | Eden | S0| S1| | 大对象直接进入老年代 | 内存) | |
| | +------+---+---+ | | | |
| +-------------------+--------------------+--------+ |
+-------------------------------------------------------++-------------------------------------------------------+
| 堆 (Heap) |
| |
| +-------------------+--------------------+--------+ |
| | 新生代 | | | |
| | (Young Gen) | 老年代 | 元空间 | |
| | | (Old Gen) | | |
| | +------+---+---+ | | (本地 | |
| | | Eden | S0| S1| | 大对象直接进入老年代 | 内存) | |
| | +------+---+---+ | | | |
| +-------------------+--------------------+--------+ |
+-------------------------------------------------------+新生代 → 老年代晋升条件:
- 年龄阈值:默认 15 岁(可通过
-XX:MaxTenuringThreshold设置)。 - 动态年龄判定:Survivor 中同龄对象大小总和超过 Survivor 空间的一半时,大于等于该年龄的对象直接进入老年代。
- 大对象直接进入老年代:通过
-XX:PretenureSizeThreshold设置。
3.1.2 对象分配流程
new 对象请求
|
v
+---- 是否栈上分配? -----------+
| 是 | 逃逸分析 + 标量替换 |--> 在栈上分配
+ +-------------------------+
| 否
v
+---- TLAB 是否可用? ----------+
| 是 | 线程本地分配缓冲区 |--> 在 Eden 的 TLAB 中分配
+ +-------------------------+
| 否
v
+---- 是否大对象? -------------+
| 是 | -XX:PretenureSizeThreshold |--> 直接在老年代分配
+ +-------------------------+
| 否
v
在 Eden 区分配(可能需要 Minor GC 腾出空间)new 对象请求
|
v
+---- 是否栈上分配? -----------+
| 是 | 逃逸分析 + 标量替换 |--> 在栈上分配
+ +-------------------------+
| 否
v
+---- TLAB 是否可用? ----------+
| 是 | 线程本地分配缓冲区 |--> 在 Eden 的 TLAB 中分配
+ +-------------------------+
| 否
v
+---- 是否大对象? -------------+
| 是 | -XX:PretenureSizeThreshold |--> 直接在老年代分配
+ +-------------------------+
| 否
v
在 Eden 区分配(可能需要 Minor GC 腾出空间)3.1.3 TLAB(Thread Local Allocation Buffer)
- 作用:每个线程在 Eden 区分配一块私有的缓冲区,避免多线程竞争同步。
- 开启:默认开启(
-XX:+UseTLAB)。 - 大小:通过
-XX:TLABSize设置,JVM 会根据线程数量动态调整。
// TLAB 相关参数
// -XX:+UseTLAB 启用 TLAB(默认开启)
// -XX:TLABSize=512k 设置 TLAB 大小
// -XX:ResizeTLAB 允许 JVM 动态调整 TLAB 大小
// -XX:TLABRefillWasteFraction=64 TLAB 放弃分配阈值// TLAB 相关参数
// -XX:+UseTLAB 启用 TLAB(默认开启)
// -XX:TLABSize=512k 设置 TLAB 大小
// -XX:ResizeTLAB 允许 JVM 动态调整 TLAB 大小
// -XX:TLABRefillWasteFraction=64 TLAB 放弃分配阈值3.1.4 元空间(Metaspace)
JDK 8 使用元空间替代永久代(PermGen)。元空间存储类的元数据、常量池、静态变量等。
+-------------------------+ +-------------------------+
| 永久代 (PermGen) | | 元空间 (Metaspace) |
| JDK 7 及之前 | | JDK 8+ |
| | | |
| - 在堆内 | | - 在本地内存(Native) |
| - 大小固定 | | - 可动态扩展 |
| - 容易 OOM | | - 默认无上限(建议设上限) |
| - MaxPermSize | | - MaxMetaspaceSize |
+-------------------------+ +-------------------------++-------------------------+ +-------------------------+
| 永久代 (PermGen) | | 元空间 (Metaspace) |
| JDK 7 及之前 | | JDK 8+ |
| | | |
| - 在堆内 | | - 在本地内存(Native) |
| - 大小固定 | | - 可动态扩展 |
| - 容易 OOM | | - 默认无上限(建议设上限) |
| - MaxPermSize | | - MaxMetaspaceSize |
+-------------------------+ +-------------------------+为什么用元空间替代永久代:
- 字符串常量池:在永久代中容易触发 Full GC,移至堆中更方便管理。
- 类型信息大小难预测:永久代大小难以设置,容易导致 OOM。
- 永久代回收效率低:元空间使用本地内存,由操作系统管理。
- 促进 HotSpot 与 JRockit 合并:JRockit 没有永久代。
元空间关键参数:
-XX:MetaspaceSize=256m # 元空间初始大小(触发 Full GC 的阈值)
-XX:MaxMetaspaceSize=512m # 元空间最大值,避免无限占用内存
-XX:MinMetaspaceFreeRatio=40 # GC 后元空间最小空闲比例
-XX:MaxMetaspaceFreeRatio=70 # GC 后元空间最大空闲比例-XX:MetaspaceSize=256m # 元空间初始大小(触发 Full GC 的阈值)
-XX:MaxMetaspaceSize=512m # 元空间最大值,避免无限占用内存
-XX:MinMetaspaceFreeRatio=40 # GC 后元空间最小空闲比例
-XX:MaxMetaspaceFreeRatio=70 # GC 后元空间最大空闲比例3.2 虚拟机栈
线程私有,生命周期与线程相同。每个方法执行时,JVM 会同步创建一个栈帧(Stack Frame)。
3.2.1 栈帧结构
+---------------------------------------+
| 栈帧 (Stack Frame) |
| |
| +----------------------------------+ |
| | 局部变量表 (Local Variables) | |
| | - this 引用 (实例方法) | |
| | - 方法参数 | |
| | - 局部变量 | |
| | - 容量:Slot(4 字节) | |
| +----------------------------------+ |
| | 操作数栈 (Operand Stack) | |
| | - 字节码指令的操作对象 | |
| | - 后进先出 (LIFO) | |
| | - 最大深度编译期确定 | |
| +----------------------------------+ |
| | 动态链接 (Dynamic Linking) | |
| | - 指向运行时常量池的方法引用 | |
| +----------------------------------+ |
| | 方法返回地址 (Return Address) | |
| | - 调用者的下一条指令地址 | |
| +----------------------------------+ |
+---------------------------------------++---------------------------------------+
| 栈帧 (Stack Frame) |
| |
| +----------------------------------+ |
| | 局部变量表 (Local Variables) | |
| | - this 引用 (实例方法) | |
| | - 方法参数 | |
| | - 局部变量 | |
| | - 容量:Slot(4 字节) | |
| +----------------------------------+ |
| | 操作数栈 (Operand Stack) | |
| | - 字节码指令的操作对象 | |
| | - 后进先出 (LIFO) | |
| | - 最大深度编译期确定 | |
| +----------------------------------+ |
| | 动态链接 (Dynamic Linking) | |
| | - 指向运行时常量池的方法引用 | |
| +----------------------------------+ |
| | 方法返回地址 (Return Address) | |
| | - 调用者的下一条指令地址 | |
| +----------------------------------+ |
+---------------------------------------+3.2.2 局部变量表
编译时确定容量,以 Slot 为单位(每个 Slot 32 位):
public void example(int a, long b, String c) {
// 局部变量表索引:
// 0: this
// 1: a (int, 1 Slot)
// 2-3: b (long, 2 Slots)
// 4: c (reference, 1 Slot)
int d = 10; // Slot 5
long e = 20L; // Slot 6-7
}public void example(int a, long b, String c) {
// 局部变量表索引:
// 0: this
// 1: a (int, 1 Slot)
// 2-3: b (long, 2 Slots)
// 4: c (reference, 1 Slot)
int d = 10; // Slot 5
long e = 20L; // Slot 6-7
}3.2.3 操作数栈
字节码指令的工作区:
public int add(int a, int b) {
return a + b;
}
// 字节码分析:
// 0: iload_1 // 将局部变量表 Slot 1 (a) 压入操作数栈
// 1: iload_2 // 将局部变量表 Slot 2 (b) 压入操作数栈
// 2: iadd // 弹出栈顶两个 int,相加后压回栈顶
// 3: ireturn // 返回栈顶的 int 值public int add(int a, int b) {
return a + b;
}
// 字节码分析:
// 0: iload_1 // 将局部变量表 Slot 1 (a) 压入操作数栈
// 1: iload_2 // 将局部变量表 Slot 2 (b) 压入操作数栈
// 2: iadd // 弹出栈顶两个 int,相加后压回栈顶
// 3: ireturn // 返回栈顶的 int 值3.2.4 栈上分配与逃逸分析
逃逸分析:JIT 编译器分析对象的作用域,判断对象是否逃逸出方法或线程。
public class EscapeAnalysisDemo {
// 对象不逃逸:可以在栈上分配
public String noEscape() {
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
return sb.toString(); // sb 本身没有逃逸,仅返回了 String 结果
}
// 对象逃逸:必须在堆上分配
public StringBuilder escape() {
StringBuilder sb = new StringBuilder();
sb.append("hello");
return sb; // sb 作为返回值逃逸出了方法
}
// 全局逃逸:赋值给静态变量
private static Object globalObj;
public void globalEscape() {
globalObj = new Object(); // Object 逃逸到全局
}
// 线程逃逸:赋值给实例字段,其他线程可访问
private Object field;
public void threadEscape() {
field = new Object(); // Object 可能被其他线程访问
}
}public class EscapeAnalysisDemo {
// 对象不逃逸:可以在栈上分配
public String noEscape() {
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
return sb.toString(); // sb 本身没有逃逸,仅返回了 String 结果
}
// 对象逃逸:必须在堆上分配
public StringBuilder escape() {
StringBuilder sb = new StringBuilder();
sb.append("hello");
return sb; // sb 作为返回值逃逸出了方法
}
// 全局逃逸:赋值给静态变量
private static Object globalObj;
public void globalEscape() {
globalObj = new Object(); // Object 逃逸到全局
}
// 线程逃逸:赋值给实例字段,其他线程可访问
private Object field;
public void threadEscape() {
field = new Object(); // Object 可能被其他线程访问
}
}基于逃逸分析的优化:
- 栈上分配:不逃逸的对象直接在栈上分配,随方法结束自动销毁,减少 GC 压力。
- 标量替换:将对象拆散为基本类型,在局部变量表中分配。
- 同步消除:如果对象不会逃逸出线程,可去掉同步锁。
// 同步消除示例
public void lockElimination() {
Object obj = new Object(); // obj 不逃逸
synchronized (obj) { // 这个锁可以被 JIT 消除
System.out.println("no contention");
}
}// 同步消除示例
public void lockElimination() {
Object obj = new Object(); // obj 不逃逸
synchronized (obj) { // 这个锁可以被 JIT 消除
System.out.println("no contention");
}
}3.2.5 栈溢出
public class StackOverflowDemo {
private int depth = 0;
// 无终止递归 → StackOverflowError
public void infiniteRecursion() {
depth++;
infiniteRecursion();
}
public static void main(String[] args) {
StackOverflowDemo demo = new StackOverflowDemo();
try {
demo.infiniteRecursion();
} catch (StackOverflowError e) {
System.out.println("栈深度: " + demo.depth);
// 常见深度(默认 1MB 栈):约 10000-20000
}
}
}public class StackOverflowDemo {
private int depth = 0;
// 无终止递归 → StackOverflowError
public void infiniteRecursion() {
depth++;
infiniteRecursion();
}
public static void main(String[] args) {
StackOverflowDemo demo = new StackOverflowDemo();
try {
demo.infiniteRecursion();
} catch (StackOverflowError e) {
System.out.println("栈深度: " + demo.depth);
// 常见深度(默认 1MB 栈):约 10000-20000
}
}
}栈大小通过 -Xss 设置:
-Xss256k # 最小栈大小(通常建议 256k - 1m)
-Xss1m # 默认值(通常为 1MB,依赖平台)-Xss256k # 最小栈大小(通常建议 256k - 1m)
-Xss1m # 默认值(通常为 1MB,依赖平台)3.3 程序计数器(PC Register)
线程私有,记录当前线程正在执行的字节码指令地址。
- 如果执行的是 Java 方法,PC 指向当前执行指令的地址。
- 如果执行的是 Native 方法,PC 值为
undefined。 - 是 JVM 规范中唯一没有规定 OOM 情况的区域。
3.4 本地方法栈(Native Method Stack)
线程私有,为 JVM 执行 Native 方法(通常是 C/C++ 实现)提供服务。
- HotSpot 将本地方法栈和虚拟机栈合二为一。
- 同样会抛出
StackOverflowError和OutOfMemoryError。
4. 对象内存布局
在 HotSpot 虚拟机中,对象在堆内存中的存储布局可以分为三部分:
+----------------------------------------------------+
| 对象内存布局 |
| |
| +---------------------------+-------------------+ |
| | 对象头 (Header) | | |
| | +--------+-------------+ | 实例数据 | |
| | |Mark | Klass | | (Instance Data) | |
| | |Word | Pointer | | | |
| | |(8/12B) | (4/8B) | | 字段值 | |
| | +--------+-------------+ | | |
| +---------------------------+-------------------+ |
| | 对齐填充 (Padding) | |
| | (8 字节对齐) | |
| +----------------------------------------------+ |
+----------------------------------------------------++----------------------------------------------------+
| 对象内存布局 |
| |
| +---------------------------+-------------------+ |
| | 对象头 (Header) | | |
| | +--------+-------------+ | 实例数据 | |
| | |Mark | Klass | | (Instance Data) | |
| | |Word | Pointer | | | |
| | |(8/12B) | (4/8B) | | 字段值 | |
| | +--------+-------------+ | | |
| +---------------------------+-------------------+ |
| | 对齐填充 (Padding) | |
| | (8 字节对齐) | |
| +----------------------------------------------+ |
+----------------------------------------------------+4.1 对象头(Object Header)
Mark Word
存储对象自身的运行时数据,在 32 位和 64 位 JVM 中长度不同。
64 位 JVM Mark Word 布局:
+-------------------+------------------+-------------+
| 状态 | Mark Word (64位) | 说明 |
+-------------------+------------------+-------------+
| 无锁 (Normal) | 0x0000000000000001| hash:25, age:4|
| | | biased:0, tag:01|
| 偏向锁 (Biased) | 0x0000000000000005| thread:54, epoch:2|
| | | age:4, biased:1, tag:01|
| 轻量级锁 (Light) | 0x0000000000000000| ptr:62, tag:00 |
| 重量级锁 (Heavy) | 0x0000000000000000| ptr:62, tag:10 |
| GC 标记 | 0x0000000000000000| ptr:62, tag:11 |
+-------------------+------------------+-------------++-------------------+------------------+-------------+
| 状态 | Mark Word (64位) | 说明 |
+-------------------+------------------+-------------+
| 无锁 (Normal) | 0x0000000000000001| hash:25, age:4|
| | | biased:0, tag:01|
| 偏向锁 (Biased) | 0x0000000000000005| thread:54, epoch:2|
| | | age:4, biased:1, tag:01|
| 轻量级锁 (Light) | 0x0000000000000000| ptr:62, tag:00 |
| 重量级锁 (Heavy) | 0x0000000000000000| ptr:62, tag:10 |
| GC 标记 | 0x0000000000000000| ptr:62, tag:11 |
+-------------------+------------------+-------------+锁升级过程(不可逆):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁无锁 → 偏向锁 → 轻量级锁 → 重量级锁JDK 15 开始默认禁用了偏向锁(-XX:-UseBiasedLocking),因为偏向锁在当代高并发场景下的维护成本高于其收益。
Klass Pointer
指向对象的类元数据指针,JVM 通过该指针确定对象是哪个类的实例。
4.2 实例数据(Instance Data)
存储对象的真正有效信息,即类中定义的各种字段。存储顺序受分配策略参数影响:
- 相同宽度字段分配在一起(long/double、int、short/char、byte/boolean、reference)。
- 父类字段在子类字段之前(HotSpot 默认)。
4.3 对齐填充(Padding)
HotSpot 要求对象起始地址必须是 8 字节的整数倍。如果实例数据部分没有对齐,就需要通过填充来补全。
4.4 指针压缩
从 JDK 6 开始,64 位 JVM 支持指针压缩(默认开启,堆小于 32GB 时)。
-XX:+UseCompressedOops # 启用指针压缩(默认开启)
-XX:-UseCompressedOops # 手动关闭-XX:+UseCompressedOops # 启用指针压缩(默认开启)
-XX:-UseCompressedOops # 手动关闭原理:64 位 JVM 中的引用指针原本占用 8 字节,开启压缩后占用 4 字节。由于对象是 8 字节对齐的,JVM 可以通过 4 字节指针左移 3 位(乘以 8)来寻址最大 32GB 的堆。
影响:
- 减少内存占用。
- 提升缓存命中率。
- 当堆大于 32GB 时,指针压缩自动失效。
对象大小计算示例:
public class ObjectSizeDemo {
// 空对象(只有对象头)
// Mark Word: 8B + Klass Pointer: 4B(压缩) + Padding: 0 = 12B 对齐到 16B
private int x; // +4B = 16B,无需填充
private boolean y; // +1B = 17B,对齐到 24B
private String ref; // +4B(压缩指针) = 21B,对齐到 24B
private long z; // +8B = 32B(按宽度分组,long 在后)
}public class ObjectSizeDemo {
// 空对象(只有对象头)
// Mark Word: 8B + Klass Pointer: 4B(压缩) + Padding: 0 = 12B 对齐到 16B
private int x; // +4B = 16B,无需填充
private boolean y; // +1B = 17B,对齐到 24B
private String ref; // +4B(压缩指针) = 21B,对齐到 24B
private long z; // +8B = 32B(按宽度分组,long 在后)
}查看对象布局:
# 使用 JOL (Java Object Layout) 工具
# Maven 依赖: org.openjdk.jol:jol-core
java -jar jol-cli.jar internals java.lang.String# 使用 JOL (Java Object Layout) 工具
# Maven 依赖: org.openjdk.jol:jol-core
java -jar jol-cli.jar internals java.lang.String5. GC 算法
5.1 三色标记法
根可达性分析的底层算法,用于并发标记:
白色:未被标记的对象(GC 不可达)
灰色:自身被标记,但引用对象尚未被标记
黑色:自身和引用对象都已被标记白色:未被标记的对象(GC 不可达)
灰色:自身被标记,但引用对象尚未被标记
黑色:自身和引用对象都已被标记// 三色标记流程
// 1. 初始:所有对象为白色
// 2. 从 GC Roots 开始遍历,将其放入灰色集合
// 3. 取出灰色对象,标记为黑色,扫描其引用
// 4. 被引用的白色对象标记为灰色
// 5. 重复 3-4,直到灰色集合为空
// 6. 剩余白色对象即为不可达对象// 三色标记流程
// 1. 初始:所有对象为白色
// 2. 从 GC Roots 开始遍历,将其放入灰色集合
// 3. 取出灰色对象,标记为黑色,扫描其引用
// 4. 被引用的白色对象标记为灰色
// 5. 重复 3-4,直到灰色集合为空
// 6. 剩余白色对象即为不可达对象并发标记的问题:对象漏标(误将存活对象标记为垃圾)。
条件:标记过程中,发生以下两种情况同时满足:
1. 黑色对象指向了白色对象(新增引用)
2. 灰色对象指向该白色对象的引用被删除
结果:白色对象实际可达,但被当作垃圾回收
解决方案:
- CMS:增量更新 (Incremental Update)
- G1:SATB (Snapshot At The Beginning)
- ZGC:读屏障 + 染色指针条件:标记过程中,发生以下两种情况同时满足:
1. 黑色对象指向了白色对象(新增引用)
2. 灰色对象指向该白色对象的引用被删除
结果:白色对象实际可达,但被当作垃圾回收
解决方案:
- CMS:增量更新 (Incremental Update)
- G1:SATB (Snapshot At The Beginning)
- ZGC:读屏障 + 染色指针5.2 标记-清除(Mark-Sweep)
初始状态: [A] [B] [C] [D] [E] 存活: A, C, E
| | | | |
标记之后: [A] [ ] [C] [ ] [E]
清除之后: [A] [ ] [C] [ ] [E]
↓
内存碎片: A 的空间 | 空闲碎片 | C 的空间 | 空闲碎片 | E 的空间初始状态: [A] [B] [C] [D] [E] 存活: A, C, E
| | | | |
标记之后: [A] [ ] [C] [ ] [E]
清除之后: [A] [ ] [C] [ ] [E]
↓
内存碎片: A 的空间 | 空闲碎片 | C 的空间 | 空闲碎片 | E 的空间优点:
- 算法简单,实现容易。
- 不需要移动对象。
缺点:
- 产生内存碎片,可能导致大对象无法分配,触发 Full GC。
- 分配效率下降(需要维护空闲列表)。
5.3 标记-整理(Mark-Compact)
初始状态: [A] [B] [C] [D] [E] 存活: A, C, E
清除之后: [A] [ ] [C] [ ] [E]
↓
整理之后: [A] [C] [E] [ ] [ ]
↓
连续空间: 存活对象紧挨在一起,空闲空间连续初始状态: [A] [B] [C] [D] [E] 存活: A, C, E
清除之后: [A] [ ] [C] [ ] [E]
↓
整理之后: [A] [C] [E] [ ] [ ]
↓
连续空间: 存活对象紧挨在一起,空闲空间连续优点:
- 无内存碎片。
- 分配速度快(使用指针碰撞)。
缺点:
- 移动对象需要 STW(Stop-The-World),老年代 GC 暂停时间长。
- 需要更新所有引用,计算量大。
5.4 复制(Copying)
初始状态: [A] [B] [C] [D] [E] 存活: A, C, E
from-space to-space (空)
↓
复制存活对象:
[A] [C] [E] [ ] [ ]
to-space (新的 from) from-space (清空)初始状态: [A] [B] [C] [D] [E] 存活: A, C, E
from-space to-space (空)
↓
复制存活对象:
[A] [C] [E] [ ] [ ]
to-space (新的 from) from-space (清空)优点:
- 只遍历存活对象,效率高。
- 无碎片,分配用指针碰撞。
缺点:
- 需要两倍空间。
- 如果存活率高,复制量大。
应用:新生代 Eden / Survivor 的默认算法。
5.5 分代收集理论
基于两个弱分代假设:
- 弱分代假说:绝大多数对象朝生夕灭。
- 强分代假说:熬过越多次 GC 的对象越难回收。
跨代引用处理:
- 老年代对象可能引用新生代对象。
- 引入记忆集(Remembered Set),避免扫描整个老年代。
6. 垃圾回收器(重点)
6.1 回收器总览
新生代 老年代
+----------+ +----------+
Serial | Serial |---(Serial Old)--> Old | 客户端 / 单线程
+----------+ +----------+
+----------+ +----------+
Parallel | ParNew |---(CMS)------> CMS | 响应优先
+----------+ +----------+
+----------+ +----------+
Throughput| Parallel |---(Parallel Old)--> Old | 吞吐量优先
+----------+ +----------+
+-----------+
G1 | | G1 | JDK 9+ 默认
+-----------+
+-----------+
ZGC | | ZGC | JDK 17+ 超大堆
+-----------+ 新生代 老年代
+----------+ +----------+
Serial | Serial |---(Serial Old)--> Old | 客户端 / 单线程
+----------+ +----------+
+----------+ +----------+
Parallel | ParNew |---(CMS)------> CMS | 响应优先
+----------+ +----------+
+----------+ +----------+
Throughput| Parallel |---(Parallel Old)--> Old | 吞吐量优先
+----------+ +----------+
+-----------+
G1 | | G1 | JDK 9+ 默认
+-----------+
+-----------+
ZGC | | ZGC | JDK 17+ 超大堆
+-----------+6.2 回收器对比表
| 回收器 | 线程数 | 算法 | STW | 适用场景 | 启用参数 |
|---|---|---|---|---|---|
| Serial | 1 | 复制 | 长 | 客户端、小内存 | -XX:+UseSerialGC |
| Serial Old | 1 | 标记-整理 | 长 | CMS 后备方案 | (与 Serial 搭配) |
| ParNew | N | 复制 | 长 | CMS 搭档 | -XX:+UseParNewGC |
| Parallel Scavenge | N | 复制 | 长 | 吞吐量优先 | -XX:+UseParallelGC |
| Parallel Old | N | 标记-整理 | 长 | 吞吐量优先 | -XX:+UseParallelOldGC |
| CMS | N | 标记-清除 | 短 | 响应优先 | -XX:+UseConcMarkSweepGC |
| G1 | N | 复制+标记-整理 | 可预测 | Server 端 | -XX:+UseG1GC |
| ZGC | N | 并发标记-整理 | <1ms | 超大堆 | -XX:+UseZGC |
6.3 Serial / Serial Old
最简单的回收器组合,单线程工作,GC 时暂停所有用户线程。
# 启用 Serial + Serial Old
-XX:+UseSerialGC# 启用 Serial + Serial Old
-XX:+UseSerialGC适用场景:
- 单核 CPU 或小内存环境。
- 堆小于 100MB 的客户端应用。
- 作为其他回收器的后备方案。
6.4 ParNew
Serial 的多线程版本,能充分利用多核 CPU。是 CMS 回收器的新生代搭档。
-XX:+UseParNewGC # 启用 ParNew + CMS(JDK 8 常用)
-XX:ParallelGCThreads=4 # 并行线程数-XX:+UseParNewGC # 启用 ParNew + CMS(JDK 8 常用)
-XX:ParallelGCThreads=4 # 并行线程数6.5 Parallel Scavenge / Parallel Old
吞吐量优先回收器,目标是达到可控制的吞吐量。
-XX:+UseParallelGC # 启用 Parallel Scavenge + Parallel Old
-XX:ParallelGCThreads=4 # GC 线程数
-XX:MaxGCPauseMillis=200 # 最大 GC 暂停时间(毫秒)
-XX:GCTimeRatio=19 # 吞吐量 = 1/(1+GCTimeRatio) = 5%
-XX:+UseAdaptiveSizePolicy # 开启自适应调整(GC 自适应的调节 Eden/Survivor 比例)-XX:+UseParallelGC # 启用 Parallel Scavenge + Parallel Old
-XX:ParallelGCThreads=4 # GC 线程数
-XX:MaxGCPauseMillis=200 # 最大 GC 暂停时间(毫秒)
-XX:GCTimeRatio=19 # 吞吐量 = 1/(1+GCTimeRatio) = 5%
-XX:+UseAdaptiveSizePolicy # 开启自适应调整(GC 自适应的调节 Eden/Survivor 比例)与 ParNew 的区别:
- ParNew 注重减少暂停时间,适合交互应用。
- Parallel Scavenge 注重吞吐量,适合后台计算任务。
吞吐量计算公式:
吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC 时间)吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC 时间)例如 -XX:GCTimeRatio=19 表示允许 1/(19+1) = 5% 的时间用于 GC。
6.6 CMS(Concurrent Mark Sweep)
以获取最短回收停顿时间为目标的回收器,适合互联网站点等对响应时间敏感的应用。
6.6.1 CMS 工作流程
初始标记 (Initial Mark) ----STW---- 标记 GC Roots 直接关联的对象(快)
|
v
并发标记 (Concurrent Mark) ----并发--- 从 GC Roots 开始三色标记(长)
|
v
重新标记 (Remark) ----STW---- 处理并发阶段变动的对象(增量更新)
|
v
并发清除 (Concurrent Sweep) ----并发--- 清除垃圾对象(不需要移动对象)
|
v
并发重置 (Concurrent Reset) ----并发--- 重置数据结构初始标记 (Initial Mark) ----STW---- 标记 GC Roots 直接关联的对象(快)
|
v
并发标记 (Concurrent Mark) ----并发--- 从 GC Roots 开始三色标记(长)
|
v
重新标记 (Remark) ----STW---- 处理并发阶段变动的对象(增量更新)
|
v
并发清除 (Concurrent Sweep) ----并发--- 清除垃圾对象(不需要移动对象)
|
v
并发重置 (Concurrent Reset) ----并发--- 重置数据结构-XX:+UseConcMarkSweepGC # 启用 CMS
-XX:CMSInitiatingOccupancyFraction=75 # 老年代 75% 时启动 CMS(默认 92%)
-XX:+UseCMSInitiatingOccupancyOnly # 始终使用设定的阈值
-XX:+CMSScavengeBeforeRemark # 重新标记前进行一次 Young GC
-XX:CMSFullGCsBeforeCompaction=5 # 5 次 Full GC 后做一次整理-XX:+UseConcMarkSweepGC # 启用 CMS
-XX:CMSInitiatingOccupancyFraction=75 # 老年代 75% 时启动 CMS(默认 92%)
-XX:+UseCMSInitiatingOccupancyOnly # 始终使用设定的阈值
-XX:+CMSScavengeBeforeRemark # 重新标记前进行一次 Young GC
-XX:CMSFullGCsBeforeCompaction=5 # 5 次 Full GC 后做一次整理6.6.2 CMS 的缺点
- CPU 敏感:并发阶段占用 CPU,可能影响用户线程。
- 浮动垃圾:并发清理阶段产生的垃圾无法在本次清理,留待下次。
- 内存碎片:标记-清除算法导致碎片,可能触发 Serial Old 后备方案("Concurrent Mode Failure")。
- 空间预留:需要预留一部分空间给并发收集期间分配的对象。
Concurrent Mode Failure:
- 原因:并发收集期间老年代空间不足
- 后果:暂停用户线程,使用 Serial Old 进行 Full GC(长时间 STW)
- 预防:降低 CMSInitiatingOccupancyFraction,提前触发 CMSConcurrent Mode Failure:
- 原因:并发收集期间老年代空间不足
- 后果:暂停用户线程,使用 Serial Old 进行 Full GC(长时间 STW)
- 预防:降低 CMSInitiatingOccupancyFraction,提前触发 CMSCMS 在 JDK 9 被标记为 deprecated,JDK 14 被正式移除。
6.7 G1(Garbage First)
JDK 9 及之后的默认垃圾回收器,将堆划分为多个大小相等的 Region。
6.7.1 Region 设计
Heap 被划分为 ~2048 个 Region,每个 Region 大小为 1MB ~ 32MB
+----+----+----+----+----+----+----+----+
| E | E | S | H | O | O | E | E |
+----+----+----+----+----+----+----+----+
| O | E | E | S | O | O | E | E |
+----+----+----+----+----+----+----+----+
| E | E | O | O | H | E | E | S |
+----+----+----+----+----+----+----+----+
E = Eden Region(新生代)
S = Survivor Region
O = Old Region(老年代)
H = Humongous Region(大对象,> Region 50%)Heap 被划分为 ~2048 个 Region,每个 Region 大小为 1MB ~ 32MB
+----+----+----+----+----+----+----+----+
| E | E | S | H | O | O | E | E |
+----+----+----+----+----+----+----+----+
| O | E | E | S | O | O | E | E |
+----+----+----+----+----+----+----+----+
| E | E | O | O | H | E | E | S |
+----+----+----+----+----+----+----+----+
E = Eden Region(新生代)
S = Survivor Region
O = Old Region(老年代)
H = Humongous Region(大对象,> Region 50%)Region 大小计算:
# 自动计算:堆大小 / 2048
# 最小 1MB,最大 32MB
# 手动设置:
-XX:G1HeapRegionSize=4m# 自动计算:堆大小 / 2048
# 最小 1MB,最大 32MB
# 手动设置:
-XX:G1HeapRegionSize=4m6.7.2 RSet(Remembered Set)
每个 Region 有一个 RSet,记录哪些 Region 中的对象引用本 Region 的对象。
Region A (Eden) Region B (Old)
+------------------+ +------------------+
| 对象 objA |----->| 对象 objB |
+------------------+ +------------------+
|
v
Region B 的 RSet:
[Region A, 偏移量 x]Region A (Eden) Region B (Old)
+------------------+ +------------------+
| 对象 objA |----->| 对象 objB |
+------------------+ +------------------+
|
v
Region B 的 RSet:
[Region A, 偏移量 x]RSet 的意义:
- 在 Young GC 时,只需扫描老年代 Region 的 RSet,而不需要扫描整个老年代。
- 用空间换时间,精确地找到跨代引用。
RSet 的存储:使用哈希表,Key 是 Region 编号,Value 是卡页索引的集合(卡表)。
6.7.3 SATB(Snapshot At The Beginning)
G1 用于并发标记的算法,解决三色标记中的对象漏标问题。
SATB 核心思想:
1. 并发标记开始时,给堆拍一个"快照"(逻辑快照,实际记录变更)
2. 标记过程中,对象引用变化被记录到 SATB 队列
3. 最终标记阶段,处理 SATB 队列中的引用变更
4. 所有在快照中存活的对象都被视为存活
优点:不需要重新扫描整个堆,效率高
缺点:可能保留部分浮动垃圾(本应回收的对象被保留)SATB 核心思想:
1. 并发标记开始时,给堆拍一个"快照"(逻辑快照,实际记录变更)
2. 标记过程中,对象引用变化被记录到 SATB 队列
3. 最终标记阶段,处理 SATB 队列中的引用变更
4. 所有在快照中存活的对象都被视为存活
优点:不需要重新扫描整个堆,效率高
缺点:可能保留部分浮动垃圾(本应回收的对象被保留)6.7.4 G1 工作流程
Young GC (持续发生)
↓ 对象晋升
Mixed GC (并发标记完成后)
↓ 回收失败
Full GC (降级到 Serial Old,长时间 STW)Young GC (持续发生)
↓ 对象晋升
Mixed GC (并发标记完成后)
↓ 回收失败
Full GC (降级到 Serial Old,长时间 STW)Young GC:
- 选定 Eden 区中所有 Region。
- 计算暂停时间目标,决定是否需要加入 Survivor 区。
- 将存活对象复制到 Survivor Region 或老年代 Region。
- 清空原来的 Eden Region。
Mixed GC:
在并发标记完成后,不仅清理新生代,还回收部分高收益的老年代 Region。
触发条件:
1. 并发标记完成(InitiatingHeapOccupancyPercent,默认 45%)
2. 回收优先级:G1 优先回收垃圾最多的 Region
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用百分比
-XX:G1MixedGCLiveThresholdPercent=85 # Region 存活对象低于此值才回收
-XX:G1MixedGCCountTarget=8 # Mixed GC 的目标次数
-XX:G1HeapWastePercent=5 # 可浪费的堆百分比,触发 Mixed GC触发条件:
1. 并发标记完成(InitiatingHeapOccupancyPercent,默认 45%)
2. 回收优先级:G1 优先回收垃圾最多的 Region
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用百分比
-XX:G1MixedGCLiveThresholdPercent=85 # Region 存活对象低于此值才回收
-XX:G1MixedGCCountTarget=8 # Mixed GC 的目标次数
-XX:G1HeapWastePercent=5 # 可浪费的堆百分比,触发 Mixed GCFull GC:
G1 的 Full GC 是单线程串行回收,非常慢。应尽量避免。
Full GC 触发原因:
1. 并发标记失败 (Concurrent Mode Failure)
2. 晋升失败 (Promotion Failed):Young GC 时 Survivor/Old 放不下晋升对象
3. 大对象分配失败 (Humongous Allocation Failure)
4. Evacuation Failure:Region 复制时空间不足Full GC 触发原因:
1. 并发标记失败 (Concurrent Mode Failure)
2. 晋升失败 (Promotion Failed):Young GC 时 Survivor/Old 放不下晋升对象
3. 大对象分配失败 (Humongous Allocation Failure)
4. Evacuation Failure:Region 复制时空间不足6.7.5 G1 调优参数
# 核心参数
-XX:+UseG1GC # 启用 G1
-XX:MaxGCPauseMillis=200 # 目标最大暂停时间(默认 200ms)
-XX:G1HeapRegionSize=4m # Region 大小
-XX:ParallelGCThreads=4 # 并行 GC 线程数
-XX:ConcGCThreads=2 # 并发 GC 线程数
# 重要调优参数
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用(默认 45%)
-XX:G1NewSizePercent=5 # 新生代初始占比
-XX:G1MaxNewSizePercent=60 # 新生代最大占比
-XX:G1ReservePercent=10 # 预留空间,避免晋升失败
-XX:G1HeapWastePercent=5 # 可浪费堆百分比
-XX:G1MixedGCCountTarget=8 # Mixed GC 目标次数
-XX:+UnlockExperimentalVMOptions
-XX:G1MixedGCLiveThresholdPercent=85 # Mixed GC 回收 Region 的存活阈值
# 日志(JDK 9+)
-Xlog:gc*:file=g1-gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug
-Xlog:gc+ergo*=trace # G1 自适应调整日志# 核心参数
-XX:+UseG1GC # 启用 G1
-XX:MaxGCPauseMillis=200 # 目标最大暂停时间(默认 200ms)
-XX:G1HeapRegionSize=4m # Region 大小
-XX:ParallelGCThreads=4 # 并行 GC 线程数
-XX:ConcGCThreads=2 # 并发 GC 线程数
# 重要调优参数
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用(默认 45%)
-XX:G1NewSizePercent=5 # 新生代初始占比
-XX:G1MaxNewSizePercent=60 # 新生代最大占比
-XX:G1ReservePercent=10 # 预留空间,避免晋升失败
-XX:G1HeapWastePercent=5 # 可浪费堆百分比
-XX:G1MixedGCCountTarget=8 # Mixed GC 目标次数
-XX:+UnlockExperimentalVMOptions
-XX:G1MixedGCLiveThresholdPercent=85 # Mixed GC 回收 Region 的存活阈值
# 日志(JDK 9+)
-Xlog:gc*:file=g1-gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug
-Xlog:gc+ergo*=trace # G1 自适应调整日志6.8 ZGC(JDK 11+)
ZGC 是一款以低延迟(亚毫秒级暂停)为目标的高性能垃圾回收器,适用于大内存(几百 GB 到 TB 级)场景。
6.8.1 ZGC 核心特性
- 暂停时间不超过 1ms(与堆大小无关)。
- 支持 8MB ~ 16TB 的堆。
- 并发标记-整理,不产生碎片。
- 染色指针(Colored Pointer) 技术。
- 读屏障(Load Barrier) 技术。
6.8.2 染色指针(Colored Pointer)
ZGC 将 64 位指针的高几位用作"颜色"标记,存储对象状态信息。
64 位地址空间布局 (x86-64):
+----+----+----+----+----+----+----+----+
| 63 | 62 | 61 | 60 | 59-48 | 47-0 |
+----+----+----+----+----+----+----+----+
| M0 | M1 | R | R | 保留 | 偏移量 |
+----+----+----+----+----+----+----+----+
M0/M1: Mark Bit(标记位,标记对象是否存活)
R: Remapped Bit(重映射位,指向对象的新地址)64 位地址空间布局 (x86-64):
+----+----+----+----+----+----+----+----+
| 63 | 62 | 61 | 60 | 59-48 | 47-0 |
+----+----+----+----+----+----+----+----+
| M0 | M1 | R | R | 保留 | 偏移量 |
+----+----+----+----+----+----+----+----+
M0/M1: Mark Bit(标记位,标记对象是否存活)
R: Remapped Bit(重映射位,指向对象的新地址)好处:
- 不需要对象头中的 GC 标记位,对象头更小。
- 对象的存活信息可以直接从指针读取,不需要访问对象本身。
- 标记和重映射完全并发进行,不暂停用户线程。
限制:
- 需要操作系统支持 64 位寻址(但只用了 47 位,JVM 保留高 4 位给染色指针)。
- 不支持 32 位平台。
- 不支持指针压缩(因为需要全部 64 位)。
6.8.3 读屏障(Load Barrier)
ZGC 在读对象引用时插入屏障逻辑(在 ldc、getfield、arrayload 等指令处)。
读屏障执行逻辑:
1. 读取对象引用指针
2. 检查指针的染色位,判断对象状态
3. 如果指针需要修正(对象被移动),则进行自愈 (Self-Healing)
4. 返回修正后的指针
"自愈":后续读取同一引用时不再需要修正,性能逐渐提升读屏障执行逻辑:
1. 读取对象引用指针
2. 检查指针的染色位,判断对象状态
3. 如果指针需要修正(对象被移动),则进行自愈 (Self-Healing)
4. 返回修正后的指针
"自愈":后续读取同一引用时不再需要修正,性能逐渐提升读屏障的核心是"三态修正":
| 指针状态 | 含义 | 操作 |
|---|---|---|
| Good | 指向最新地址 | 直接使用 |
| Bad (M0/M1) | 对象在标记阶段 | 标记并修正 |
| Bad (Remapped) | 对象被移动 | 修正为新地址 |
6.8.4 ZGC 工作流程
暂停:初始标记 (STW, ~0.1ms)
↓
并发:并发标记 (Concurrent Mark)
↓
暂停:最终标记 (STW, ~0.1ms) + 处理弱引用
↓
并发:并发重映射 (Concurrent Remap) + 并发整理
↓
(下一次 GC 从并发重映射阶段开始,避免额外遍历)暂停:初始标记 (STW, ~0.1ms)
↓
并发:并发标记 (Concurrent Mark)
↓
暂停:最终标记 (STW, ~0.1ms) + 处理弱引用
↓
并发:并发重映射 (Concurrent Remap) + 并发整理
↓
(下一次 GC 从并发重映射阶段开始,避免额外遍历)6.8.5 分代 ZGC(JDK 21+)
ZGC 原版本(单代)对新生代对象和老年代对象一视同仁,导致新生代对象频繁晋升,触发频繁的并发标记。
分代 ZGC 的改进:
分代 ZGC (ZGenerational):
+---------------------------+
| 年轻代 (Young Gen) | ← 频繁扫描,大部分对象短命
| - 特供年轻代的染色指针 |
+---------------------------+
| 老年代 (Old Gen) | ← 较少扫描,长寿命对象
| - 特供老年代的染色指针 |
+---------------------------+
优点:
- 新生代 GC 更频繁但更快
- 老年代 GC 更少,整体延迟更低
- 不需要 Full GC(传统 ZGC 在极端情况下需要)分代 ZGC (ZGenerational):
+---------------------------+
| 年轻代 (Young Gen) | ← 频繁扫描,大部分对象短命
| - 特供年轻代的染色指针 |
+---------------------------+
| 老年代 (Old Gen) | ← 较少扫描,长寿命对象
| - 特供老年代的染色指针 |
+---------------------------+
优点:
- 新生代 GC 更频繁但更快
- 老年代 GC 更少,整体延迟更低
- 不需要 Full GC(传统 ZGC 在极端情况下需要)# 启用 ZGC
-XX:+UseZGC
# 启用分代 ZGC(JDK 21+ 默认)
-XX:+UseZGC -XX:+ZGenerational
# ZGC 关键参数
-XX:ZAllocationSpikeTolerance=2.0 # 分配波动容忍度
-XX:ZCollectionInterval=300 # 最大 GC 间隔(秒)
-XX:ZFragmentationLimit=25 # 碎片占比上限(%)
-Xlog:gc*:file=zgc.log:time,uptime # GC 日志# 启用 ZGC
-XX:+UseZGC
# 启用分代 ZGC(JDK 21+ 默认)
-XX:+UseZGC -XX:+ZGenerational
# ZGC 关键参数
-XX:ZAllocationSpikeTolerance=2.0 # 分配波动容忍度
-XX:ZCollectionInterval=300 # 最大 GC 间隔(秒)
-XX:ZFragmentationLimit=25 # 碎片占比上限(%)
-Xlog:gc*:file=zgc.log:time,uptime # GC 日志6.8.6 ZGC vs G1 对比
| 对比维度 | G1 | ZGC |
|---|---|---|
| 暂停时间 | 几十到几百毫秒 | <1ms(与堆大小无关) |
| 实现技术 | Region + RSet + SATB | 染色指针 + 读屏障 |
| 内存限制 | 最大 ~64GB(综合推荐) | 最大 16TB |
| 指针压缩 | 支持 | 不支持 |
| 分代 | 新生代 + 老年代 | 分代 ZGC (JDK 21+) |
| 适用堆大小 | 4GB ~ 64GB | >64GB,大内存场景 |
| JDK 版本 | JDK 9+ 默认 | JDK 11+ 实验,15+ 正式 |
| CPU 开销 | 较低 | 略高(读屏障开销) |
7. JVM 调优(重点)
7.1 内存参数
堆参数
# 堆大小
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与 Xms 相等,避免扩容开销)
-Xmn2g # 新生代大小
-XX:SurvivorRatio=8 # Eden : Survivor = 8 : 1 : 1
# 老年代
-XX:NewRatio=2 # 老年代 : 新生代 = 2 : 1(不设置 -Xmn 时有效)
# 元空间
-XX:MetaspaceSize=256m # 初始元空间大小(避免频繁 GC)
-XX:MaxMetaspaceSize=256m # 最大元空间大小
# 大对象
-XX:PretenureSizeThreshold=1m # 大于此值的对象直接在老年代分配
# 直接内存
-XX:MaxDirectMemorySize=512m # NIO 直接内存上限(默认等于堆最大值)# 堆大小
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与 Xms 相等,避免扩容开销)
-Xmn2g # 新生代大小
-XX:SurvivorRatio=8 # Eden : Survivor = 8 : 1 : 1
# 老年代
-XX:NewRatio=2 # 老年代 : 新生代 = 2 : 1(不设置 -Xmn 时有效)
# 元空间
-XX:MetaspaceSize=256m # 初始元空间大小(避免频繁 GC)
-XX:MaxMetaspaceSize=256m # 最大元空间大小
# 大对象
-XX:PretenureSizeThreshold=1m # 大于此值的对象直接在老年代分配
# 直接内存
-XX:MaxDirectMemorySize=512m # NIO 直接内存上限(默认等于堆最大值)栈参数
-Xss256k # 线程栈大小(默认 1MB,建议 256k ~ 1m)
-XX:ThreadStackSize # 等价于 -Xss-Xss256k # 线程栈大小(默认 1MB,建议 256k ~ 1m)
-XX:ThreadStackSize # 等价于 -Xss7.2 GC 参数
选择 GC
# JDK 8 常用
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC # CMS + ParNew
-XX:+UseParallelGC -XX:+UseParallelOldGC # 吞吐量优先
-XX:+UseSerialGC # 单线程
# JDK 9+
-XX:+UseG1GC # G1(默认)
-XX:MaxGCPauseMillis=200 # G1 目标暂停时间
# JDK 17+
-XX:+UseZGC # ZGC
-XX:+ZGenerational # 分代 ZGC(JDK 21+)# JDK 8 常用
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC # CMS + ParNew
-XX:+UseParallelGC -XX:+UseParallelOldGC # 吞吐量优先
-XX:+UseSerialGC # 单线程
# JDK 9+
-XX:+UseG1GC # G1(默认)
-XX:MaxGCPauseMillis=200 # G1 目标暂停时间
# JDK 17+
-XX:+UseZGC # ZGC
-XX:+ZGenerational # 分代 ZGC(JDK 21+)通用 GC 参数
-XX:+DisableExplicitGC # 禁止 System.gc() 触发 Full GC
-XX:+ExplicitGCInvokesConcurrent # System.gc() 触发并发 GC(CMS/G1)
-XX:+PrintGCDetails # 打印 GC 详情(JDK 8)
-XX:+PrintGCDateStamps # GC 日志时间戳
-Xloggc:gc.log # GC 日志文件(JDK 8)
# JDK 9+ 统一 GC 日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug
-Xlog:gc+age=trace
-Xlog:gc+ergo*=trace # G1 自适应调整-XX:+DisableExplicitGC # 禁止 System.gc() 触发 Full GC
-XX:+ExplicitGCInvokesConcurrent # System.gc() 触发并发 GC(CMS/G1)
-XX:+PrintGCDetails # 打印 GC 详情(JDK 8)
-XX:+PrintGCDateStamps # GC 日志时间戳
-Xloggc:gc.log # GC 日志文件(JDK 8)
# JDK 9+ 统一 GC 日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug
-Xlog:gc+age=trace
-Xlog:gc+ergo*=trace # G1 自适应调整7.3 故障排查参数
-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动生成 heap dump
-XX:HeapDumpPath=/path/dump.hprof # dump 文件路径
-XX:+PrintClassHistogram # Ctrl+Break 打印类直方图
-XX:+PrintConcurrentLocks # 打印并发锁信息
-XX:+UnlockDiagnosticVMOptions # 解锁诊断参数-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动生成 heap dump
-XX:HeapDumpPath=/path/dump.hprof # dump 文件路径
-XX:+PrintClassHistogram # Ctrl+Break 打印类直方图
-XX:+PrintConcurrentLocks # 打印并发锁信息
-XX:+UnlockDiagnosticVMOptions # 解锁诊断参数7.4 调优工具
命令行工具
| 命令 | 作用 | 示例 |
|---|---|---|
jps | 列出 Java 进程 | jps -l |
jstat | JVM 统计监控 | jstat -gcutil <pid> 1000 |
jmap | 堆转储、堆信息 | jmap -dump:format=b,file=heap.hprof <pid> |
jstack | 线程栈快照 | jstack <pid> |
jinfo | JVM 配置信息 | jinfo -flags <pid> |
jcmd | 综合诊断命令 | jcmd <pid> help |
jstat 使用示例:
# 查看 GC 统计,每秒打印一次
jstat -gcutil <pid> 1000
# 输出解读
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 45.23 72.15 89.50 85.20 128 0.856 3 0.234 1.090
| | | | | | | | | | |
S0/S1 Eden 老年代 元空间 压缩类 Young Young Full Full 总GC
存活率 使用率 使用率 使用率 空间 GC次数 GC时间 GC次数 GC时间 时间# 查看 GC 统计,每秒打印一次
jstat -gcutil <pid> 1000
# 输出解读
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 45.23 72.15 89.50 85.20 128 0.856 3 0.234 1.090
| | | | | | | | | | |
S0/S1 Eden 老年代 元空间 压缩类 Young Young Full Full 总GC
存活率 使用率 使用率 使用率 空间 GC次数 GC时间 GC次数 GC时间 时间jstack 使用示例:
# 输出线程栈
jstack <pid>
# 检测死锁
jstack -l <pid> | grep "deadlock" -A 20
# 查找 CPU 最高线程
top -H -p <pid> # 找到 CPU 最高的线程 ID(十进制)
printf "%x\n" <tid> # 转换为十六进制
jstack <pid> | grep -A 30 <十六进制tid># 输出线程栈
jstack <pid>
# 检测死锁
jstack -l <pid> | grep "deadlock" -A 20
# 查找 CPU 最高线程
top -H -p <pid> # 找到 CPU 最高的线程 ID(十进制)
printf "%x\n" <tid> # 转换为十六进制
jstack <pid> | grep -A 30 <十六进制tid>jmap 使用示例:
# 生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
# 查看堆直方图(按对象类型统计)
jmap -histo <pid> | head -20
# 查看类加载信息
jmap -clstats <pid># 生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
# 查看堆直方图(按对象类型统计)
jmap -histo <pid> | head -20
# 查看类加载信息
jmap -clstats <pid>可视化工具
| 工具 | 用途 | 启动方式 |
|---|---|---|
| JConsole | JVM 监控(内存/线程/类/MBean) | jconsole |
| VisualVM | 全能分析(CPU/内存/线程/Dump) | jvisualvm |
| JMC (Java Mission Control) | 飞行记录器分析 | jmc |
| MAT (Memory Analyzer Tool) | 堆转储离线分析 | mat |
| JProfiler | 商业性能分析工具 | 独立安装 |
Arthas 在线诊断
Arthas 是阿里开源的 Java 在线诊断工具,支持在生产环境无侵入排查问题。
# 启动 Arthas
java -jar arthas-boot.jar
# 常用命令
dashboard # 系统面板(线程/内存/GC/系统)
thread -n 3 # 最忙的 3 个线程
thread -b # 查看死锁
sc -d *UserService # 搜索并反编译类
jad com.example.UserService # 在线反编译
watch com.example.UserService getUser "{params,returnObj}" -x 2 # 方法观测
trace com.example.UserService getUser # 方法调用链路追踪
monitor com.example.UserService getUser -c 5 # 5 秒内方法监控
heapdump /tmp/dump.hprof # 在线生成堆转储
vmtool --action getInstances --className java.lang.String --limit 10 # 获取实例# 启动 Arthas
java -jar arthas-boot.jar
# 常用命令
dashboard # 系统面板(线程/内存/GC/系统)
thread -n 3 # 最忙的 3 个线程
thread -b # 查看死锁
sc -d *UserService # 搜索并反编译类
jad com.example.UserService # 在线反编译
watch com.example.UserService getUser "{params,returnObj}" -x 2 # 方法观测
trace com.example.UserService getUser # 方法调用链路追踪
monitor com.example.UserService getUser -c 5 # 5 秒内方法监控
heapdump /tmp/dump.hprof # 在线生成堆转储
vmtool --action getInstances --className java.lang.String --limit 10 # 获取实例8. JIT 编译
8.1 解释执行 vs 编译执行
解释执行:
字节码 → [解释器] → 逐条翻译为机器码 → 执行
优点:启动快
缺点:执行慢
编译执行:
字节码 → [JIT 编译器] → 编译为机器码 → 缓存并执行
优点:执行快
缺点:启动慢(需要预热)解释执行:
字节码 → [解释器] → 逐条翻译为机器码 → 执行
优点:启动快
缺点:执行慢
编译执行:
字节码 → [JIT 编译器] → 编译为机器码 → 缓存并执行
优点:执行快
缺点:启动慢(需要预热)HotSpot 采用混合模式:
- 启动阶段:解释执行,让程序快速启动。
- 运行阶段:发现热点代码 → JIT 编译为本地代码 → 执行效率大幅提升。
# 查看 JIT 模式
java -version
# 输出含 "mixed mode" 表示混合模式
# 强制解释执行
-Xint
# 强制编译执行
-Xcomp# 查看 JIT 模式
java -version
# 输出含 "mixed mode" 表示混合模式
# 强制解释执行
-Xint
# 强制编译执行
-Xcomp8.2 C1 vs C2 编译器
| 特性 | C1 (Client Compiler) | C2 (Server Compiler) |
|---|---|---|
| 编译速度 | 快 | 慢 |
| 代码质量 | 一般 | 极高 |
| 优化深度 | 浅(基础优化) | 深(激进优化) |
| 适用 | 客户端应用、启动阶段 | 服务端应用、长期运行 |
8.3 分层编译(Tiered Compilation)
JDK 8+ 默认启用分层编译,结合 C1 和 C2 的优势。
分层级别:
Level 0 - 解释执行 (Interpreter)
Level 1 - C1 简单编译 (无 profiling)
Level 2 - C1 编译 + 部分 profiling
Level 3 - C1 编译 + 完全 profiling
Level 4 - C2 编译
升级路径:
0 → 3 (解释执行到 C1 完全 profiling)
3 → 4 (C1 收集 profiling 数据后升级到 C2)
如果 Level 4 排队中,可以降级到 Level 2/1分层级别:
Level 0 - 解释执行 (Interpreter)
Level 1 - C1 简单编译 (无 profiling)
Level 2 - C1 编译 + 部分 profiling
Level 3 - C1 编译 + 完全 profiling
Level 4 - C2 编译
升级路径:
0 → 3 (解释执行到 C1 完全 profiling)
3 → 4 (C1 收集 profiling 数据后升级到 C2)
如果 Level 4 排队中,可以降级到 Level 2/1-XX:-TieredCompilation # 关闭分层编译(JDK 8+ 默认开启)
-XX:TieredStopAtLevel=4 # 最高编译层级(默认 4)
-XX:Tier3CompileThreshold=2000 # Level 3 编译阈值
-XX:Tier4CompileThreshold=15000 # Level 4 编译阈值-XX:-TieredCompilation # 关闭分层编译(JDK 8+ 默认开启)
-XX:TieredStopAtLevel=4 # 最高编译层级(默认 4)
-XX:Tier3CompileThreshold=2000 # Level 3 编译阈值
-XX:Tier4CompileThreshold=15000 # Level 4 编译阈值8.4 方法内联(Inlining)
将方法调用处的代码替换为被调用方法的实际代码,减少方法调用开销。
// 内联前
public int add(int a, int b) {
return a + b;
}
public void caller() {
int result = add(1, 2);
// ...
}
// 内联后(C2 可能会做)
public void caller() {
int result = 1 + 2; // add() 方法被内联
// ...
}
// 常量折叠后
public void caller() {
int result = 3; // 编译期直接计算出结果
// ...
}// 内联前
public int add(int a, int b) {
return a + b;
}
public void caller() {
int result = add(1, 2);
// ...
}
// 内联后(C2 可能会做)
public void caller() {
int result = 1 + 2; // add() 方法被内联
// ...
}
// 常量折叠后
public void caller() {
int result = 3; // 编译期直接计算出结果
// ...
}-XX:MaxInlineSize=35 # 内联方法的最大字节码大小(默认 35)
-XX:FreqInlineSize=325 # 热点方法内联的最大字节码大小(默认 325)
-XX:+Inline # 开启内联(默认开启)
-XX:MaxTrivialSize=6 # 小方法内联阈值(6 字节)
-XX:InlineSmallCode=2000 # 方法编译代码大小超过此值不再内联-XX:MaxInlineSize=35 # 内联方法的最大字节码大小(默认 35)
-XX:FreqInlineSize=325 # 热点方法内联的最大字节码大小(默认 325)
-XX:+Inline # 开启内联(默认开启)
-XX:MaxTrivialSize=6 # 小方法内联阈值(6 字节)
-XX:InlineSmallCode=2000 # 方法编译代码大小超过此值不再内联哪些方法更可能被内联:
- 小方法(getter/setter 几乎 100% 内联)。
- 私有方法(不会被重写,可安全内联)。
- final 方法(不会被重写)。
- 静态方法。
- 热调用点(调用频率高)。
8.5 逃逸分析(Escape Analysis)
已在 3.2.3 节详细讨论。JIT 基于逃逸分析可以做的优化:
- 栈上分配:不逃逸的对象在栈上分配,省去 GC。
- 标量替换:将对象拆为基本类型在局部变量表中分配。
- 同步消除:去掉不逃逸对象的同步锁。
-XX:+DoEscapeAnalysis # 开启逃逸分析(默认开启)
-XX:+EliminateAllocations # 开启标量替换(默认开启)
-XX:+EliminateLocks # 开启同步消除(默认开启)
-XX:MaxEscapeRecursionDepth=6 # 逃逸分析递归深度-XX:+DoEscapeAnalysis # 开启逃逸分析(默认开启)
-XX:+EliminateAllocations # 开启标量替换(默认开启)
-XX:+EliminateLocks # 开启同步消除(默认开启)
-XX:MaxEscapeRecursionDepth=6 # 逃逸分析递归深度8.6 锁优化
锁粗化(Lock Coarsening)
将多个连续的锁操作合并为一个更大范围的锁:
// 锁粗化前
public void coarsenBefore() {
for (int i = 0; i < 100; i++) {
synchronized (this) { // 每次循环加锁/解锁
// 操作
}
}
}
// 锁粗化后(JIT 优化)
public void coarsenAfter() {
synchronized (this) { // 一把锁包含整个循环
for (int i = 0; i < 100; i++) {
// 操作
}
}
}// 锁粗化前
public void coarsenBefore() {
for (int i = 0; i < 100; i++) {
synchronized (this) { // 每次循环加锁/解锁
// 操作
}
}
}
// 锁粗化后(JIT 优化)
public void coarsenAfter() {
synchronized (this) { // 一把锁包含整个循环
for (int i = 0; i < 100; i++) {
// 操作
}
}
}锁消除(Lock Elimination)
当 JIT 检测到不存在锁竞争时,直接去掉锁:
public void elimination() {
// StringBuffer 是线程安全的,但在局部变量中不会发生竞争
StringBuffer sb = new StringBuffer(); // 不逃逸
sb.append("a");
sb.append("b");
// JIT 可以消除 sb 上的同步锁
// 实际优化后等效于 StringBuilder
}public void elimination() {
// StringBuffer 是线程安全的,但在局部变量中不会发生竞争
StringBuffer sb = new StringBuffer(); // 不逃逸
sb.append("a");
sb.append("b");
// JIT 可以消除 sb 上的同步锁
// 实际优化后等效于 StringBuilder
}8.7 编译日志与诊断
# 打印被编译的方法
-XX:+PrintCompilation
# 打印内联信息
-XX:+PrintInlining
# 安全点日志
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# 编译日志示例输出
# 132 1 % java.lang.String::hashCode @ 55 (67 bytes)
# | | | | |
# 时间 编译层级 方法名 方法长度
# %: 表示 OSR 编译
# !: 表示包含异常处理
# s: 表示同步方法
# n: 表示 native 方法# 打印被编译的方法
-XX:+PrintCompilation
# 打印内联信息
-XX:+PrintInlining
# 安全点日志
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# 编译日志示例输出
# 132 1 % java.lang.String::hashCode @ 55 (67 bytes)
# | | | | |
# 时间 编译层级 方法名 方法长度
# %: 表示 OSR 编译
# !: 表示包含异常处理
# s: 表示同步方法
# n: 表示 native 方法9. 性能排查实战
9.1 CPU 100% 排查
场景:服务器 CPU 飙升至 100%,接口响应变慢。
排查步骤:
# 第一步:找到 CPU 最高的 Java 进程
top
# 第二步:找到进程内 CPU 最高的线程
top -H -p <pid>
# 第三步:将线程 ID 转换为十六进制
printf "%x\n" <thread_id>
# 第四步:导出线程栈,查找对应线程
jstack <pid> | grep -A 30 <十六进制tid>
# 第五步:定位问题代码
# 可能的原因:
# 1. 死循环 / 空转(while(true) 无 sleep/yield)
# 2. 频繁的 GC(GC 线程占 CPU)
# 3. 正则表达式回溯(灾难性回溯)
# 4. 序列化/反序列化瓶颈
# 5. 频繁的锁竞争# 第一步:找到 CPU 最高的 Java 进程
top
# 第二步:找到进程内 CPU 最高的线程
top -H -p <pid>
# 第三步:将线程 ID 转换为十六进制
printf "%x\n" <thread_id>
# 第四步:导出线程栈,查找对应线程
jstack <pid> | grep -A 30 <十六进制tid>
# 第五步:定位问题代码
# 可能的原因:
# 1. 死循环 / 空转(while(true) 无 sleep/yield)
# 2. 频繁的 GC(GC 线程占 CPU)
# 3. 正则表达式回溯(灾难性回溯)
# 4. 序列化/反序列化瓶颈
# 5. 频繁的锁竞争Arthas 快速定位:
# 使用 Arthas 的 thread 命令
thread -n 3 # 展示最忙的 3 个线程,直接显示栈# 使用 Arthas 的 thread 命令
thread -n 3 # 展示最忙的 3 个线程,直接显示栈9.2 内存泄漏排查
场景:堆内存持续上升,Full GC 频繁,最终 OOM。
排查步骤:
# 第一步:确认内存泄漏还是内存不足
# 观察 GC 情况
jstat -gcutil <pid> 2000 10
# 如果 YGC 频繁,FGCT 持续增长,说明有泄漏趋势
# 第二步:生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
# 第三步:使用 MAT 分析
# 打开 heap.hprof → Leak Suspects Report
# 重点关注:
# 1. 大对象(Biggest Objects)
# 2. GC Root 路径(Path to GC Roots)
# 3. 重复对象(如 String 去重)# 第一步:确认内存泄漏还是内存不足
# 观察 GC 情况
jstat -gcutil <pid> 2000 10
# 如果 YGC 频繁,FGCT 持续增长,说明有泄漏趋势
# 第二步:生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
# 第三步:使用 MAT 分析
# 打开 heap.hprof → Leak Suspects Report
# 重点关注:
# 1. 大对象(Biggest Objects)
# 2. GC Root 路径(Path to GC Roots)
# 3. 重复对象(如 String 去重)常见内存泄漏模式:
// 1. 静态集合类持有对象
public class LeakDemo {
private static final List<Object> CACHE = new ArrayList<>();
public void add(Object obj) {
CACHE.add(obj); // 只增不减 ← 泄漏
}
}
// 2. 未关闭的资源
public void readFile() throws IOException {
InputStream in = new FileInputStream("test.txt");
// 没有 close() ← 泄漏
// 应该用 try-with-resources
}
// 3. 内部类持有外部类引用
public class Outer {
private byte[] largeData = new byte[1024 * 1024];
public class Inner {
// 内部类持有外部类的隐式引用
// 如果 Inner 对象被长期持有,Outer 无法回收
}
}
// 4. ThreadLocal 使用不当
public class ThreadLocalLeak {
private static final ThreadLocal<byte[]> TL = new ThreadLocal<>();
public void process() {
TL.set(new byte[1024 * 1024]); // 从不 remove()
// 在池化线程中使用 ThreadLocal 必须 remove()
// TL.remove();
}
}
// 5. 重写 equals/hashCode 导致 HashMap 中 key 无法删除
public class BadKey {
public int id;
// 没有重写 hashCode/equals
// 存入 HashMap 后修改 id 导致无法找到原 key → 泄漏
}// 1. 静态集合类持有对象
public class LeakDemo {
private static final List<Object> CACHE = new ArrayList<>();
public void add(Object obj) {
CACHE.add(obj); // 只增不减 ← 泄漏
}
}
// 2. 未关闭的资源
public void readFile() throws IOException {
InputStream in = new FileInputStream("test.txt");
// 没有 close() ← 泄漏
// 应该用 try-with-resources
}
// 3. 内部类持有外部类引用
public class Outer {
private byte[] largeData = new byte[1024 * 1024];
public class Inner {
// 内部类持有外部类的隐式引用
// 如果 Inner 对象被长期持有,Outer 无法回收
}
}
// 4. ThreadLocal 使用不当
public class ThreadLocalLeak {
private static final ThreadLocal<byte[]> TL = new ThreadLocal<>();
public void process() {
TL.set(new byte[1024 * 1024]); // 从不 remove()
// 在池化线程中使用 ThreadLocal 必须 remove()
// TL.remove();
}
}
// 5. 重写 equals/hashCode 导致 HashMap 中 key 无法删除
public class BadKey {
public int id;
// 没有重写 hashCode/equals
// 存入 HashMap 后修改 id 导致无法找到原 key → 泄漏
}9.3 频繁 GC 问题
场景:Young GC 或 Full GC 过于频繁,影响应用性能。
Young GC 频繁分析:
# 查看 GC 频率和时间
jstat -gcutil <pid> 1000
# 如果 YGC 每秒多次,说明新生代太小或对象创建过多
# 分析对象分配速率
jstat -gc <pid> 1000
# EU (Eden 使用量) 快速增长说明分配速率高# 查看 GC 频率和时间
jstat -gcutil <pid> 1000
# 如果 YGC 每秒多次,说明新生代太小或对象创建过多
# 分析对象分配速率
jstat -gc <pid> 1000
# EU (Eden 使用量) 快速增长说明分配速率高解决方案:
# 1. 增大新生代
-Xmn2g
-XX:SurvivorRatio=8
# 2. 调整晋升阈值,让更多短命对象在新生代回收
-XX:MaxTenuringThreshold=15
# 3. 排查业务代码,减少对象创建
# - 使用对象池(如连接池、线程池)
# - 合理使用 StringBuilder 代替 String 拼接
# - 使用基本类型避免自动装箱# 1. 增大新生代
-Xmn2g
-XX:SurvivorRatio=8
# 2. 调整晋升阈值,让更多短命对象在新生代回收
-XX:MaxTenuringThreshold=15
# 3. 排查业务代码,减少对象创建
# - 使用对象池(如连接池、线程池)
# - 合理使用 StringBuilder 代替 String 拼接
# - 使用基本类型避免自动装箱Full GC 频繁分析:
# 查看 Full GC 原因
# JDK 8: -XX:+PrintGCDetails → gc.log
# JDK 9+: -Xlog:gc*=info → gc.log
# 常见 Full GC 原因:
# 1. 老年代空间不足(晋升对象多,回收少)
# 2. 元空间不足(类加载过多)
# 3. System.gc() 调用
# 4. CMS Concurrent Mode Failure
# 5. JVM 内部错误# 查看 Full GC 原因
# JDK 8: -XX:+PrintGCDetails → gc.log
# JDK 9+: -Xlog:gc*=info → gc.log
# 常见 Full GC 原因:
# 1. 老年代空间不足(晋升对象多,回收少)
# 2. 元空间不足(类加载过多)
# 3. System.gc() 调用
# 4. CMS Concurrent Mode Failure
# 5. JVM 内部错误Full GC 优化方案:
# 1. 增大老年代或调整晋升条件
-XX:MaxTenuringThreshold=15
-XX:TargetSurvivorRatio=90
# 2. 降低触发 CMS/G1 并发标记的阈值
-XX:CMSInitiatingOccupancyFraction=60
-XX:InitiatingHeapOccupancyPercent=35
# 3. 禁用 System.gc()(如果是代码原因)
-XX:+DisableExplicitGC
# 4. 元空间设置固定大小(避免元空间 GC)
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m# 1. 增大老年代或调整晋升条件
-XX:MaxTenuringThreshold=15
-XX:TargetSurvivorRatio=90
# 2. 降低触发 CMS/G1 并发标记的阈值
-XX:CMSInitiatingOccupancyFraction=60
-XX:InitiatingHeapOccupancyPercent=35
# 3. 禁用 System.gc()(如果是代码原因)
-XX:+DisableExplicitGC
# 4. 元空间设置固定大小(避免元空间 GC)
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m9.4 死锁检测
// 死锁示例
public class DeadlockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
sleep(100); // 确保 t2 拿到 LOCK_B
synchronized (LOCK_B) {
System.out.println("t1 done");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
sleep(100); // 确保 t1 拿到 LOCK_A
synchronized (LOCK_A) {
System.out.println("t2 done");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { }
}
}// 死锁示例
public class DeadlockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
sleep(100); // 确保 t2 拿到 LOCK_B
synchronized (LOCK_B) {
System.out.println("t1 done");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
sleep(100); // 确保 t1 拿到 LOCK_A
synchronized (LOCK_A) {
System.out.println("t2 done");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { }
}
}排查方法:
# 方法一:jstack 检测
jstack -l <pid>
# 输出中有 "Found one Java-level deadlock" 字样
# 方法二:jconsole 检测
# 连接进程 → 线程 → 检测死锁
# 方法三:Arthas
thread -b # 检测死锁# 方法一:jstack 检测
jstack -l <pid>
# 输出中有 "Found one Java-level deadlock" 字样
# 方法二:jconsole 检测
# 连接进程 → 线程 → 检测死锁
# 方法三:Arthas
thread -b # 检测死锁9.5 OOM 异常场景
堆溢出(Java Heap Space)
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB
}
}
}public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB
}
}
}# 异常信息
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
# 排查
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof
# MAT 分析 dump 文件,找到泄漏对象# 异常信息
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
# 排查
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof
# MAT 分析 dump 文件,找到泄漏对象栈溢出(Stack Overflow)
public class StackOOM {
public static void main(String[] args) {
infiniteRecursion();
}
static void infiniteRecursion() {
infiniteRecursion(); // 无限递归
}
}public class StackOOM {
public static void main(String[] args) {
infiniteRecursion();
}
static void infiniteRecursion() {
infiniteRecursion(); // 无限递归
}
}# 异常信息
Exception in thread "main" java.lang.StackOverflowError
# 排查
# 检查递归调用、循环调用、复杂框架调用链
# 解决方案
# - 增大栈空间:-Xss1m
# - 修复无限递归# 异常信息
Exception in thread "main" java.lang.StackOverflowError
# 排查
# 检查递归调用、循环调用、复杂框架调用链
# 解决方案
# - 增大栈空间:-Xss1m
# - 修复无限递归元空间溢出(Metaspace)
public class MetaspaceOOM {
public static void main(String[] args) throws Exception {
while (true) {
// 使用 ASM/CGLIB 动态生成类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestBean.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor)
(obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create(); // 不断生成新类
}
}
static class TestBean {}
}public class MetaspaceOOM {
public static void main(String[] args) throws Exception {
while (true) {
// 使用 ASM/CGLIB 动态生成类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestBean.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor)
(obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create(); // 不断生成新类
}
}
static class TestBean {}
}# 异常信息
java.lang.OutOfMemoryError: Metaspace
# 常见原因
# 1. CGLIB/ASM 动态生成类(Spring AOP、MyBatis Mapper)
# 2. JSP/JSTL 热部署
# 3. 大量使用 Lambda 表达式或反射
# 解决方案
# - 限制元空间大小:-XX:MaxMetaspaceSize=512m
# - 减少不必要的动态代理
# - GC 后元空间依然增长说明泄漏# 异常信息
java.lang.OutOfMemoryError: Metaspace
# 常见原因
# 1. CGLIB/ASM 动态生成类(Spring AOP、MyBatis Mapper)
# 2. JSP/JSTL 热部署
# 3. 大量使用 Lambda 表达式或反射
# 解决方案
# - 限制元空间大小:-XX:MaxMetaspaceSize=512m
# - 减少不必要的动态代理
# - GC 后元空间依然增长说明泄漏直接内存溢出(Direct Buffer Memory)
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
// 使用 Unsafe 分配直接内存
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB); // 每次分配 1MB 直接内存
}
}
}public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
// 使用 Unsafe 分配直接内存
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB); // 每次分配 1MB 直接内存
}
}
}# 异常信息
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
# 常见原因
# 1. NIO 操作中 ByteBuffer.allocateDirect() 未释放
# 2. Netty 缓冲区使用不当(未 release)
# 排查
# - 查看直接内存使用:jcmd <pid> VM.native_memory
# - 开启 NMT 跟踪:-XX:NativeMemoryTracking=summary
# 解决方案
# - 限制直接内存:-XX:MaxDirectMemorySize=512m
# - 确保 NIO buffer 被正确释放# 异常信息
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
# 常见原因
# 1. NIO 操作中 ByteBuffer.allocateDirect() 未释放
# 2. Netty 缓冲区使用不当(未 release)
# 排查
# - 查看直接内存使用:jcmd <pid> VM.native_memory
# - 开启 NMT 跟踪:-XX:NativeMemoryTracking=summary
# 解决方案
# - 限制直接内存:-XX:MaxDirectMemorySize=512m
# - 确保 NIO buffer 被正确释放GC 时间过长(GC Overhead Limit)
# 异常信息
java.lang.OutOfMemoryError: GC overhead limit exceeded
# 含义:GC 消耗了超过 98% 的 CPU 时间,但回收不到 2% 的堆
# 实质是 JVM 对"即将 OOM"的一种提前预警
# 优化思路
# 1. 增大堆内存
# 2. 解决内存泄漏(根本原因)
# 3. 切换 GC 算法
# 禁用此检查(不推荐,仅临时应对)
-XX:-UseGCOverheadLimit# 异常信息
java.lang.OutOfMemoryError: GC overhead limit exceeded
# 含义:GC 消耗了超过 98% 的 CPU 时间,但回收不到 2% 的堆
# 实质是 JVM 对"即将 OOM"的一种提前预警
# 优化思路
# 1. 增大堆内存
# 2. 解决内存泄漏(根本原因)
# 3. 切换 GC 算法
# 禁用此检查(不推荐,仅临时应对)
-XX:-UseGCOverheadLimit10. 参数速查表
堆参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-Xms | 物理内存 1/64 | 初始堆大小 |
-Xmx | 物理内存 1/4 | 最大堆大小 |
-Xmn | 堆的 1/3 | 新生代大小 |
-XX:NewRatio | 2 | 老年代/新生代比例 |
-XX:SurvivorRatio | 8 | Eden/Survivor 比例 |
-XX:MaxTenuringThreshold | 15 | 晋升老年代年龄阈值 |
-XX:PretenureSizeThreshold | 0 | 大对象直接进入老年代的阈值 |
-XX:+UseTLAB | true | 启用线程本地分配缓冲区 |
-XX:TLABSize | 自动 | 每个 TLAB 大小 |
元空间参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:MetaspaceSize | 21M | 首次触发 Full GC 的元空间大小 |
-XX:MaxMetaspaceSize | 无上限 | 元空间最大值 |
-XX:MinMetaspaceFreeRatio | 40 | GC 后最小空闲比例 |
-XX:MaxMetaspaceFreeRatio | 70 | GC 后最大空闲比例 |
-XX:+UseCompressedClassPointers | true | 压缩类指针(与元空间配合) |
栈参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-Xss | 1M(平台相关) | 线程栈大小 |
-XX:ThreadStackSize | 1M | 等价于 -Xss |
GC 参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:+UseSerialGC | - | Serial + Serial Old |
-XX:+UseParNewGC | - | ParNew + Serial Old |
-XX:+UseParallelGC | - | Parallel Scavenge + Parallel Old |
-XX:+UseConcMarkSweepGC | - | ParNew + CMS + Serial Old 后备 |
-XX:+UseG1GC | JDK 9+ | G1 回收器 |
-XX:+UseZGC | - | ZGC 回收器 |
-XX:MaxGCPauseMillis | 200 | 目标最大 GC 暂停(ms) |
-XX:GCTimeRatio | 99 | 吞吐量目标(1/1+N) |
-XX:ParallelGCThreads | CPU 核数 | 并行 GC 线程数 |
-XX:ConcGCThreads | 并行线程数 * 3/4 | 并发 GC 线程数 |
-XX:+DisableExplicitGC | false | 禁止 System.gc() |
G1 专用参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:G1HeapRegionSize | 1-32M | Region 大小 |
-XX:InitiatingHeapOccupancyPercent | 45 | 触发并发标记的堆占用百分比 |
-XX:G1NewSizePercent | 5 | 新生代最小占比 |
-XX:G1MaxNewSizePercent | 60 | 新生代最大占比 |
-XX:G1ReservePercent | 10 | 晋升预留空间百分比 |
-XX:G1HeapWastePercent | 5 | Mixed GC 触发阈值 |
-XX:G1MixedGCCountTarget | 8 | Mixed GC 目标次数 |
-XX:G1MixedGCLiveThresholdPercent | 85 | Region 存活对象阈值 |
ZGC 专用参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:+UseZGC | - | 启用 ZGC |
-XX:+ZGenerational | JDK 21+ | 分代 ZGC |
-XX:ZAllocationSpikeTolerance | 2.0 | 分配波动容忍度 |
-XX:ZCollectionInterval | 最大 | ZGC 最大间隔(秒) |
-XX:ZFragmentationLimit | 25 | 最大碎片百分比 |
-XX:ZUncommit | true | 归还未使用内存给 OS |
JIT 编译参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-XX:+TieredCompilation | true | 分层编译 |
-XX:Tier4CompileThreshold | 15000 | C2 编译阈值 |
-XX:+PrintCompilation | false | 打印编译日志 |
-XX:+PrintInlining | false | 打印内联信息 |
-XX:MaxInlineSize | 35 | 内联方法最大字节码大小 |
-XX:FreqInlineSize | 325 | 热点方法内联阈值 |
-XX:+DoEscapeAnalysis | true | 逃逸分析 |
-XX:+EliminateAllocations | true | 标量替换 |
-XX:+EliminateLocks | true | 锁消除 |
-XX:-UseBiasedLocking | JDK 15+ | 偏向锁(JDK 15+ 默认禁用) |
调试参数
| 参数 | 说明 |
|---|---|
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump |
-XX:HeapDumpPath=path | dump 文件路径 |
-XX:+PrintGCDetails | 打印 GC 详情 |
-XX:+PrintGCDateStamps | GC 日志时间戳 |
-Xloggc:gc.log | GC 日志文件 |
-Xlog:gc* | JDK 9+ 统一 GC 日志 |
-XX:+UnlockDiagnosticVMOptions | 解锁诊断参数 |
-XX:NativeMemoryTracking=summary | 本地内存跟踪 |
总结
| 知识点 | 核心要点 |
|---|---|
| 类加载 | 双亲委派保障安全性,自定义打破用于隔离。加载、链接、初始化三阶段。 |
| 运行时数据区 | 堆(对象主力)、栈(帧结构 + 逃逸分析)、元空间(替代永久代)。 |
| 对象布局 | 对象头 Mark Word(Klass Pointer) + 实例数据 + 对齐填充。指针压缩节省空间。 |
| GC 理论 | 分代收集、三色标记(漏标问题)、Stop-The-World 不可避免。 |
| GC 选型 | 小内存→Serial,吞吐量→Parallel,低延迟→G1/ZGC。 |
| G1 核心 | Region + RSet + SATB。可预测暂停时间,JDK 9+ 默认。 |
| ZGC 核心 | 染色指针 + 读屏障。亚毫秒级暂停,JDK 17+ 大内存标配。 |
| JIT | 分层编译 + 方法内联 + 逃逸分析 + 锁优化。预热带来高性能。 |
| 调优 | 先诊断再优化。工具链:jstat/jmap/jstack → MAT/Arthas。 |
JVM 调优没有银弹。理解原理、使用工具、持续观察,找到最适合业务场景的配置才是正道。
Cao Note