Java语言的解释性和编译性(通过JVM 的执行引擎)
Java 代码(.java 文件)要先使用
javac
编译器编译为.class
文件(字节码),紧接着再通过JVM 的执行引擎(Execution Engine) 负责处理 Java 字节码并执行,它的主要组成部分包括:
- 解释器(Interpreter):逐行解释字节码执行,启动快但执行速度较慢。
- JIT 编译器(Just-In-Time Compiler):将热点字节码编译为本地机器码,提高执行效率。
- 垃圾回收器(Garbage Collector, GC):管理 Java 堆中的对象回收,提升内存管理效率。
(1)Java 代码先编译成字节码
- Java 代码(.java 文件)使用
javac
编译器编译为.class
文件(字节码)。 - 这种字节码与平台无关,可以在不同的 JVM 上运行。
(2)JVM 先解释执行,再逐步编译为机器码
- 当 JVM 启动 Java 应用程序时,解释器(Interpreter) 会逐条解析字节码并执行,优点是启动速度快,但缺点是运行速度慢。
- JVM 会分析热点代码(执行次数较多的代码),并使用 JIT(即时编译器) 将热点代码直接编译成机器码,提高性能。
(3)JIT 编译器优化热点代码
- JIT 编译器(如 C1、C2 编译器)会在代码执行时,将热点字节码转换为本地机器码,提升执行速度。
- 解释执行 + JIT 编译 的混合模式确保 Java 既有较快的启动速度,又能提升长时间运行的性能。
总结:
解释器负责启动时快速执行,JIT 编译器负责优化热点代码。 这就是 Java 既有解释语言的灵活性,又有编译语言的高效性的原因。
1. JVM 运行时数据区(Runtime Data Areas)
1.1. 程序计数器(Program Counter Register)
概述
程序计数器(PC Register)是 JVM 中一个小型的内存区域,它的作用是记录当前线程正在执行的字节码指令地址。
特点
- 线程私有(每个线程都有一个独立的 PC 寄存器)。
- 存储字节码指令地址,用于指示当前线程下一条要执行的指令。
- 执行 Java 方法时,PC 计数器存储正在执行的字节码指令地址。
- 执行本地方法(Native Method)时,PC 计数器值
undefined
(未定义)。 - 该区域是 JVM 唯一不会发生 OutOfMemoryError 的区域。
作用
- 记录当前线程的执行位置(类似于 CPU 的 PC 寄存器)。
- 线程切换时,保证线程恢复后能继续执行正确的指令。
- 实现 Java 代码的多线程并发(JVM 通过线程轮流切换,每个线程都有自己的 PC 寄存器)。
1.2. Java 虚拟机栈(JVM Stack)
概述
Java 虚拟机栈(JVM Stack)用于存储 Java 方法执行时的栈帧(Stack Frame),是 Java 方法执行的基础。
特点
- 线程私有,每个线程有独立的栈。
- 栈的大小可以通过
-Xss
参数设置(例如-Xss1M
设置栈大小为 1MB)。 - 栈的生命周期与线程相同,线程结束时栈也随之销毁。
栈帧(Stack Frame)的组成
每个栈帧对应一个正在执行的方法,包含:
-
局部变量表(Local Variable Table)
- 存放方法中的基本数据类型(int、long、float、double 等)和对象引用(reference)。
- 局部变量表的大小在编译期确定,运行时不会改变。
long
和double
类型占 两个 存储单元,其它数据类型占 一个 存储单元。
-
操作数栈(Operand Stack)
- 作为字节码指令的操作数临时存储区,用于方法调用时传递参数、计算临时结果等。
- JVM 指令基于栈操作(如
iadd
指令会从操作数栈中弹出两个整数相加后再压入栈中)。
-
动态链接(Dynamic Linking)
- 指向方法区中的运行时常量池,用于解析方法引用(即方法调用时如何找到方法地址)。
-
方法返回地址(Return Address)
- 记录方法执行完毕后,返回到调用者的位置,以便继续执行。
栈的空间大小
- 栈的大小由
-Xss
参数控制,通常 默认 1MB,可根据需求调整:java -Xss512k MyApplication
- 每个线程的栈大小 512KB。
- 栈过大可能导致
StackOverflowError
(递归调用过深)。
为什么栈是从高地址向低地址增长?
-
LIFO 机制(后进先出)
- 栈用于存储方法调用信息,每次调用新方法时,会创建一个栈帧(Stack Frame),压入栈顶。
- 方法执行完后,栈帧被弹出,栈顶回到上一个方法的栈帧。
-
CPU 设计 & 指针运算优化
- 许多计算机体系结构(如 x86、ARM)都使用“向下增长”的栈。
- 这允许
ESP
(栈指针寄存器)直接递减来分配新的栈帧,提高性能。
可能出现的异常
- StackOverflowError:递归过深导致栈空间耗尽。
- OutOfMemoryError:JVM 栈的大小动态扩展失败(一般在线程数量过多时)。
1.3. 本地方法栈(Native Method Stack)
概述
本地方法栈(Native Method Stack)与 JVM 栈类似,但它用于存储 Native 方法的执行信息。
特点
- 线程私有,生命周期与线程相同。
- 主要用于 JNI(Java Native Interface)调用 C/C++ 代码。
- 可能出现:
StackOverflowError
OutOfMemoryError
作用
- 维护 Native 方法调用时的参数、返回地址、Native 方法执行环境等。
- 例如,调用
System.arraycopy()
方法时,JVM 需要通过本地方法栈进入 C 代码执行内存拷贝。
1.4. 堆(Heap)
概述
堆(Heap)是 JVM 内存中最大的区域,用于存储所有对象实例。
特点
- 线程共享(所有线程都能访问)。
- GC(垃圾回收器)管理的区域。
- 堆的大小可通过
-Xms
(初始大小)和-Xmx
(最大大小)参数控制。 - 可能抛出
OutOfMemoryError: Java heap space
。
堆的分代
- 新生代(Young Generation)
- Eden 区(对象最初分配在这里)。
- Survivor 0(S0)、Survivor 1(S1)(少量存活对象在两者之间交替存储)。
- 老年代(Old Generation)
- 长期存活对象存放这里,GC 频率较低。
垃圾回收
- Minor GC(新生代 GC):采用复制算法(对象生命周期短,适合快速回收)清理 Eden 和 Survivor 区。
- Major GC / Full GC:清理整个堆,采用标记-整理算法(对象生命周期长)清理老年代。
- Major GC主要针对老年代进行清理,Full GC对整个堆内存(包括年轻代、老年代以及元空间)进行回收。
堆的空间大小
-
堆的大小可以通过 JVM 参数设置:
-Xms
:堆的初始大小(默认通常是 1/64 物理内存)-Xmx
:堆的最大大小(默认通常是 1/4 物理内存)- 例如:
java -Xms256m -Xmx1024m MyApplication
- 最小堆内存 256MB
- 最大堆内存 1024MB
-
默认情况下,堆的大小会随着 GC 调整,但不能超过
-Xmx
设定的上限。
为什么堆是从低地址向高地址增长?
-
堆是动态分配的,大小不固定
- JVM 通过
-Xms
(最小堆)和-Xmx
(最大堆)设置堆的大小。 - 由于对象的创建是动态的,JVM 需要扩展堆的大小,通常向高地址扩展。
- JVM 通过
-
操作系统内存管理
- 在 C 语言的
malloc()
和JVM
的new
语义中,分配的堆空间通常从低地址向高地址增长。 - 堆的增长方向使得堆的可扩展性更好,能动态调整大小。
- 在 C 语言的
1.5. 方法区(Method Area,又称元空间 Metaspace)
概述
存储类的元数据(方法信息、静态变量、运行时常量池)。关于类的信息存储在这里。
JDK 7 及以前
- 方法区是堆中的永久代(PermGen)。
- 受
-XX:PermSize
和-XX:MaxPermSize
限制。
JDK 8 及以后
- 方法区改为单独的元空间(Metaspace)。
- 使用本地内存,不再受堆大小限制。
-XX:MetaspaceSize
控制其大小。
1.6 运行时常量池(在方法区中)字符串常量池(在堆中)
常量池类型 | 存储位置(JDK 6 及以前) | 存储位置(JDK 7+) | 存储内容 | 作用 |
---|---|---|---|---|
类文件常量池(Class File Constant Pool) | .class 文件 | .class 文件 | 字面量(数值、字符串)、符号引用 | 编译时生成,在运行时加载到运行时常量池 |
运行时常量池(Runtime Constant Pool) | 方法区(永久代 PermGen) | 方法区(元空间 Metaspace) | 从类文件加载的常量(字面量、符号引用)、运行时生成的常量(String.intern() ) | 动态解析符号引用、存储运行时常量 |
字符串常量池(String Pool) | 方法区(永久代 PermGen) | 堆(Heap) | String 字面量、intern() 方法存入的字符串 | 优化字符串存储,减少内存占用 |
常量类型 | 说明 |
---|---|
字面量 | 编译时生成的常量,如 final 修饰的常量、字符串字面量、数值(int、float、double、long)等。 |
符号引用 | 类名、字段名、方法名的符号引用(未解析为具体地址),用于支持动态链接。 |
方法引用 | 方法的符号引用,如方法的名称、描述符等。 |
1.7 总结
内存区域 | 线程私有/共享 | 主要作用 | 可能抛出的异常 |
---|---|---|---|
程序计数器 | 线程私有 | 记录当前线程执行的字节码地址 | 无 |
JVM 栈 | 线程私有 | 存储局部变量表、操作数栈 | StackOverflowError 、OutOfMemoryError |
本地方法栈 | 线程私有 | 执行 Native 方法 | StackOverflowError 、OutOfMemoryError |
堆 | 线程共享 | 存储对象实例 | OutOfMemoryError: Java heap space |
方法区(元空间) | 线程共享 | 存储类信息、静态变量 | OutOfMemoryError: Metaspace |
直接内存 | 线程共享 | 用于高效 I/O(如 NIO) | OutOfMemoryError: Direct Buffer Memory |
2. 类加载机制
2.1 类加载器
(1)启动类加载器(Bootstrap ClassLoader)
- 负责加载JDK 核心类库(
rt.jar
,charsets.jar
等)。 - 由 C++ 代码实现,无法直接获取其实例(即
null
)。 - 只能加载JDK 自带的核心类库,无法加载用户定义的
.class
文件。
主要加载的类包括:
java.lang.*
(如String
、Integer
、System
)java.util.*
(如ArrayList
、HashMap
)java.io.*
(如File
、InputStream
)java.nio.*
、java.net.*
等
(2)扩展类加载器(ExtClassLoader)
- 负责加载
lib/ext/
目录下的扩展类库(如javax.crypto.*
)。 - 由 Java 代码实现,可通过
ClassLoader.getSystemClassLoader().getParent()
获取。 - JDK 9 以后,扩展类加载器被移除,改为平台类加载器(PlatformClassLoader)。
主要加载的类包括:
javax.crypto.*
(加密库)javax.sound.*
(声音处理库)javax.imageio.*
(图像处理库)
(3)应用类加载器(AppClassLoader/ SystemClassLoader)
- 默认的类加载器:如果你没有手动指定类加载器,默认由它加载。
- 可以通过
ClassLoader.getSystemClassLoader()
获取到它。 - 支持动态加载 JAR 包:当你添加 JAR 依赖(如 Spring Boot 依赖的 JAR),它会动态加载这些类。
主要加载的类包括:
com.example.MyClass
org.springframework.*
- 任何放在
classpath
下的.class
文件
(4)自定义类加载器
- 默认类加载器仅加载
classpath
下的类,如果需要从网络、数据库、加密文件中加载.class
文件,必须使用自定义类加载器。 - 默认的
AppClassLoader
共享classpath
,如果多个模块的类存在相同的包名,可能会发生类冲突。 - 防止 Java 反编译:Java
.class
文件容易被反编译,我们可以加密.class
文件,并使用自定义类加载器在运行时解密。
如何自定义类加载器?
-
方式 1:继承
ClassLoader。
Java 提供了ClassLoader
抽象类,允许我们创建自己的类加载器。 -
方式 2:继承
URLClassLoader
。如果.class
文件存放在 JAR 或远程服务器上,我们可以继承URLClassLoader
来动态加载类。
2.2 双亲委派机制(Parent Delegation Model)
1. 什么是双亲委派机制?
双亲委派机制 是指 类加载器在加载一个类时,先委托其父类加载器加载,只有当父类加载器无法加载该类时,子类加载器才会尝试自己加载。
2. 双亲委派机制的工作流程
- 当一个
ClassLoader
需要加载一个类时,它不会自己直接加载,而是先委托给父类加载器。 - 父类加载器递归向上委托,最终到
Bootstrap ClassLoader
(顶层)。 - 如果
Bootstrap ClassLoader
无法加载该类(即不是核心类库),那么父类加载器会逐层返回,直到应用类加载器(AppClassLoader
)。 - 如果所有的父类加载器都无法加载该类,那么当前类加载器才会自己尝试加载。
3. 为什么要使用双亲委派机制?
✅ 保证 Java 运行时的安全性
- 避免核心 API 被篡改(
java.lang.String
始终由Bootstrap ClassLoader
加载)。 - 防止类的重复加载和类冲突。
✅ 提高类加载的效率
- 先尝试加载已经加载过的类,避免重复加载。
2.3 类加载过程
类加载的五个阶段
阶段 | 说明 |
---|---|
加载(Loading) | 读取 .class 文件,将字节码转换为 Class 对象。 |
验证(Verification) | 检查字节码是否合法,防止恶意代码执行。 |
准备(Preparation) | 分配静态变量的内存,初始化默认值(不赋具体值)。 |
解析(Resolution) | 将符号引用转换为直接引用(方法地址、变量地址)。 |
初始化(Initialization) | 执行 static 代码块,赋值静态变量。 |
2.3.1. 加载(Loading)
步骤:
- 读取
.class
文件(从硬盘、网络、JAR 包等加载类的字节码)。 - 转换字节码为 JVM 识别的 Class 对象,存入方法区。
- 在堆(Heap)中创建 Class 对象,表示该类的运行时信息。
示例
Class<?> clazz = Class.forName("java.lang.String");
Class.forName()
方法会触发类加载。
2.3.2. 连接(Linking)
(1)验证(Verification)
- 检查
.class
文件格式是否正确,防止恶意代码(字节码验证)。 - 例如,检查字节码指令是否合法,是否会破坏 JVM 运行。
(2)准备(Preparation)
- 为类的静态变量 分配内存,并设置默认值(如
int
变量默认值为0
)。 - 例如:
public static int a = 10; // 在准备阶段 a=0,在初始化阶段才变成10
(3)解析(Resolution)
- 将符号引用(如
java.lang.String
)转换为直接引用(JVM 内存地址)。
2.3.3. 初始化(Initialization)
- 执行
<clinit>()
静态代码块,初始化静态变量(准备阶段是为静态变量赋默认值,而这里是要设置你所定义的值)。 - 只有第一次使用该类时才执行初始化,确保类只初始化一次。
示例
class Example {
static {
System.out.println("Static block executed");
}
public static int value = 10;
}
public class Test {
public static void main(String[] args) {
System.out.println(Example.value);
}
}
3. Java 对象的创建过程
当 Java 代码执行 new
关键字创建对象时,JVM 需要完成以下步骤:
步骤 1:检查类是否已被加载
- JVM 先检查目标类的元信息是否已加载到方法区(元空间 Metaspace):
- 如果 类未加载,JVM 先通过 类加载器(ClassLoader) 加载
.class
文件,并完成 类加载、验证、准备、解析、初始化 过程(即 类的五个生命周期阶段)。 - 如果 类已加载,跳过此步骤。
- 如果 类未加载,JVM 先通过 类加载器(ClassLoader) 加载
步骤 2:为对象分配内存
JVM 在堆(Heap)中为新对象分配内存,分配策略取决于 内存是否连续:
-
指针碰撞(Bump the Pointer)(内存连续时):
- 堆内存按顺序分配,JVM 仅需将指针移动到新的可用地址。
- 适用于 使用 GC 压缩后 的堆。
-
空闲列表(Free List)(内存不连续时):
- 维护空闲内存块列表,找到合适的内存块进行分配。
- 适用于 堆内存碎片较多 的情况。
-
线程私有分配缓冲区(TLAB, Thread Local Allocation Buffer):
- JVM 允许每个线程在堆中新建私有缓存区域,提高对象分配效率,减少同步锁竞争。
步骤 3:初始化对象的默认值
- JVM 将对象字段初始化为默认值(不调用构造方法)。
class Example {
int x; // 默认值 0
boolean y; // 默认值 false
String s; // 默认值 null
}
这里需要注意,类加载过程中也会有对变量赋默认值的操作,但二者是不同的,类加载过程中的是为类的静态变量赋默认值,而这里是对对象的属性进行赋默认值。
步骤 4:设置对象的元数据
-
Mark Word(标记字段)
- 存储 哈希码、GC 状态、锁信息。
- 在对象加锁、GC 过程中会被修改。
-
Class Pointer(类指针)
- 指向对象所属的类元信息(方法区中的
Class
对象)。 - 通过此指针可以找到对象的类型信息。
- 指向对象所属的类元信息(方法区中的
步骤 5:执行构造方法
JVM 调用 构造方法 <init>()
,执行初始化逻辑:
class Example {
int num;
Example() {
num = 10;
System.out.println("Constructor executed!");
}
}
public class Test {
public static void main(String[] args) {
Example obj = new Example();
}
}
- JVM 调用构造方法,
num = 10
。
这里需要注意,类加载过程中的赋值操作与这里不同,类加载过程中只是单纯为类的静态变量赋值,而这里是调用构造函数对对象的属性进行赋值。
4. Java 对象的内存分配
4.1 对象的内存结构
Java 采用 堆 + 栈 的模式进行对象的内存管理。
存储位置 | 存储内容 |
---|---|
堆(Heap) | 对象本身(实例变量、数组) |
栈(Stack) | 对象引用(局部变量表) |
方法区(Method Area) | 类的元信息(方法、静态变量) |
方法区:
A 类的静态变量 b = 20
A 类的方法信息
栈:
obj1 -> 指向堆中的 A 对象
obj2 -> 指向另一个 A 对象
堆:
obj1 的实例变量 a = 10
obj2 的实例变量 a = 10
4.2 具体对象内存分配示例
class Person {
static String species = "Human"; // 静态变量(方法区)
String name; // 实例变量(堆)
int age; // 实例变量(堆)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sayHello() {
System.out.println("Hello, my name is " + name);
}
}
public class MemoryDemo {
public static void main(String[] args) {
Person p1 = new Person("Alice", 25); // 在堆中创建对象
Person p2 = new Person("Bob", 30); // 在堆中创建另一个对象
p1.sayHello();
p2.sayHello();
}
}
直观的内存示意图
方法区(存储类信息 + 静态变量)
-------------------------------------------------
| 类名:Person
| 静态变量:species = "Human"
| 方法:sayHello()
-------------------------------------------------
栈(存储局部变量/对象引用)
---------------------------------
| main() 方法的栈帧
| p1 -> 指向 堆中的对象 1
| p2 -> 指向 堆中的对象 2
---------------------------------
堆(存储对象实例)
-------------------------------------
| Person 对象 1 (p1)
| name = "Alice"
| age = 25
-------------------------------------
| Person 对象 2 (p2)
| name = "Bob"
| age = 30
-------------------------------------
4.3 对象的内存分配策略
JVM 根据对象的生命周期和大小,决定其分配的位置:
-
新生代(Young Generation)
- 大部分对象先在 Eden 区分配,如果对象存活过 GC,则进入 Survivor 区。
- 新生代 GC(Minor GC)频繁执行,但速度快。
-
老年代(Old Generation)
- 生命周期长的对象 会晋升到老年代(如缓存对象)。
- 当大对象无法放入新生代,直接进入老年代。
-
栈上分配(逃逸分析)
- JVM 可能优化对象分配,将短生命周期对象存放在栈上,减少 GC 压力。
- 需要开启
-XX:+DoEscapeAnalysis
5. JVM 垃圾回收机制(GC)
5.1 为什么需要垃圾回收?
Java 采用自动内存管理
- 在 C 语言中,开发者需要手动申请和释放内存(
malloc()
/free()
),容易导致 内存泄漏(Memory Leak) 或 悬空指针(Dangling Pointer)。 - 在 Java 中,JVM 通过 GC 自动回收不再使用的对象,避免手动管理内存的复杂性。
解决对象生命周期管理问题
- Java 采用 堆(Heap)存储对象,但对象的生命周期不同:
- 短生命周期对象(局部变量、循环创建的对象)
- 长生命周期对象(缓存、全局对象)
- 永久对象(
static
变量)
- GC 需要智能回收短生命周期对象,并优化长期存活对象的管理。
5.2 判断对象是否需要垃圾回收的两种方法
5.2.1. 引用计数法(已淘汰)
- 原理:每个对象有一个引用计数器,引用 +1,解除引用 -1,当引用计数变为 0,说明对象可被回收。
- 缺陷:由于无法处理循环引用(两个对象相互引用,但不再使用)的问题,现已淘汰。
5.2.2 可达性分析法
基本原理
可达性分析法是基于图遍历(Graph Traversal)的方式进行垃圾对象检测:
- GC Roots(垃圾回收根对象) 作为起点(根节点)。
- 从 GC Roots 开始遍历所有可以访问到的对象,标记为存活。
- 未被遍历到的对象 被认为是不可达(Garbage),可以被回收。
什么是 GC Roots(垃圾回收根对象) / GC Roots 的来源
在可达性分析中,JVM 会选择一组特殊的对象作为根对象(GC Roots),从这些根开始查找所有可达对象。
GC Roots 类型 | 存储位置 | 示例 |
---|---|---|
栈帧中的局部变量 | 栈(Stack) | 方法内的局部变量 Object obj = new Object(); |
静态变量(Static) | 方法区(Metaspace) | static Object obj = new Object(); |
常量池中的引用 | 方法区(Metaspace) | String s = "Hello"; |
JNI(Native 方法)引用的对象 | 本地方法栈(Native Stack) | 通过 JNI 访问的 Java 对象 |
线程对象 | 线程管理区 | 运行中的线程对象 Thread.currentThread() |
5.3 垃圾回收算法
JVM 采用不同的 GC 算法来优化垃圾回收,主要包括:
GC 算法 | 原理 | 优缺点 |
---|---|---|
标记-清除(Mark-Sweep) | 标记存活对象 → 清除未标记对象 | 产生内存碎片,影响分配效率 |
复制(Copying) | 复制存活对象到新区域,清空旧区域 | 内存利用率低(50% 内存浪费) |
标记-整理(Mark-Compact) | 标记存活对象 → 移动对象(向一端移动) → 回收未使用空间 | 解决碎片问题,性能较高 |
分代回收(Generational GC) | 新生代 采用复制算法,老年代 采用标记整理算法 | 适用于大规模应用 |
-
新生代(Young Generation)
- 采用复制算法(对象生命周期短,适合快速回收)。
- 包括 Eden、Survivor 0、Survivor 1。
- Minor GC 发生在新生代,速度快。
-
老年代(Old Generation)
- 采用标记-整理算法(对象生命周期长)。
- Major GC / Full GC 发生在老年代,通常比 Minor GC 慢。
5.4 常见垃圾回收器
GC | 新生代算法 | 老年代算法 | 适用场景 |
---|---|---|---|
Serial GC | 复制 | 标记整理 | 单线程,适用于小型应用 |
Parallel GC | 复制 | 标记整理 | 多线程高吞吐量 |
CMS GC | 标记清除 | 标记清除 | 低延迟,适用于 Web 应用 |
G1 GC | Region 化管理 | Region 化管理 | 大内存应用,JDK 9+ 推荐 |
ZGC | 并发 | 并发 | 超低延迟,JDK 11+ |
STW(Stop-The-World)的概念
STW(Stop-The-World) 是指 JVM 在执行 GC 时,会暂停所有应用线程,以便垃圾回收器安全地回收对象。这意味着:
- 所有应用线程停止执行,等待 GC 完成后再继续运行。
- STW 发生时,Java 代码暂停执行,系统响应变慢,可能导致卡顿。
5.4.1 CMS GC(Concurrent Mark-Sweep)
CMS(Concurrent Mark-Sweep)是 JDK 1.4 引入的 低延迟 GC,适用于Web 服务器、金融系统等低停顿时间应用。
(1)CMS GC 的核心特点
✅ 最小化 STW(低延迟),适用于交互式应用。
✅ 并发执行 GC,不影响应用线程运行。
✅ "标记-清除" 算法,回收时不会整理堆内存(容易产生内存碎片)。
❌ 垃圾碎片问题严重,可能导致 Full GC(STW 变长)。
❌ CPU 资源开销大,GC 线程与应用线程竞争 CPU 资源。
(2)CMS GC 的工作原理
1️⃣ CMS GC 的堆内存结构
- 采用 "分代回收"(Generational GC)策略:
- 新生代(Young Generation):使用 "复制" 算法进行垃圾回收(Minor GC)。
- 老年代(Old Generation):使用 "标记-清除" 算法进行垃圾回收(Major GC)。
- 方法区。
2️⃣ CMS GC 的垃圾回收流程
CMS GC 的核心思想是:并发执行垃圾回收,尽可能减少 STW 时间。
这里的并发执行指的是和应用线程并发执行,不用暂停应用线程也能进行垃圾回收。
垃圾回收流程如下:
-
初始标记(Initial Mark,STW):标记 GC Roots 直接关联的对象,STW 时间短。
-
并发标记(Concurrent Marking):在应用程序运行的同时,遍历对象图,标记可达对象。
-
重新标记(Remark,STW):由于并发标记时,可能有对象状态发生变化,因此需要再次 STW,重新标记存活对象。
-
并发清除(Concurrent Sweep):在应用程序运行的同时,并发清除垃圾对象,释放内存。
-
Full GC(当 CMS GC 失败时触发,STW 时间长):由于 CMS 不进行内存整理(Compaction),可能导致碎片化问题。当大对象无法分配到连续空间时,触发 Full GC(可能造成严重 STW(通常几百毫秒到几秒))。
(3)CMS GC 的垃圾碎片问题
为什么 CMS GC 会产生垃圾碎片?
- CMS 采用"标记-清除"算法,不进行内存整理,导致老年代中存在很多不连续的空闲内存(碎片)。
- 当大对象需要分配时,如果没有足够的连续空间,JVM 可能触发 Full GC 进行内存整理(STW 时间长)。
解决方案:
-
参数优化
-XX:+UseCMSCompactAtFullCollection
(在 Full GC 后进行整理)。-XX:CMSFullGCsBeforeCompaction=3
(每 3 次 Full GC 后执行一次内存整理)。
-
改用 G1 GC
- G1 GC 通过 Region 化管理和混合回收,可以避免碎片化问题。
5.4.2 G1 GC(Garbage First)
G1 GC(Garbage First GC)是 JDK 7u4 引入,并在 JDK 9 成为 默认 GC。
适用于大内存应用(4GB 以上),相比 CMS GC 减少了碎片化问题,提供更可预测的 GC 停顿时间。
(1)G1 GC 的核心特点
✅ Region(分区化管理),动态调整新生代和老年代比例。
✅ 可预测的 GC 停顿时间(-XX:MaxGCPauseMillis
)。
✅ 并发执行回收,减少 STW 停顿时间。
✅ 自动整理内存(不会产生碎片化问题)。
❌ 相比 CMS,CPU 开销更高。
❌ 吞吐量略低于 Parallel GC。
(2)G1 GC 的工作原理
1️⃣ G1 GC 的堆内存结构
- 不同于 CMS GC 的 "分代管理",G1 GC 采用 "Region(分区)管理":
- Eden(新生代)
- Survivor(新生代)
- Old(老年代)
- Humongous(存放大对象)
- Free(未使用的 Region)
2️⃣ G1 GC 的垃圾回收流程
-
年轻代 GC(Minor GC,STW):复制存活对象到 Survivor 或老年代,清空 Eden。
-
并发标记(Concurrent Marking,避免 STW):识别老年代中垃圾最多的 Region,准备回收。
-
混合回收(Mixed GC,减少 Full GC):同时回收年轻代和部分老年代,减少老年代空间不足问题。
-
Full GC(极少发生):只有当 G1 GC 失败时才会触发 Full GC。
(3)G1 GC 避免垃圾碎片
- 通过 Region 化管理对象,回收垃圾最多的 Region,避免碎片化问题。
- 当需要整理时,可以逐步迁移存活对象,减少 STW 时间。
5.4.3 CMS GC vs G1 GC 对比
JDK 9默认使用G1 GC
对比项 | CMS GC | G1 GC |
---|---|---|
适用场景 | 低延迟应用(Web 服务器) | 大内存应用(4GB+) |
回收策略 | 标记-清除,不整理内存 | Region 化管理,减少碎片 |
STW 时间 | 可能较长(Full GC) | 可预测(-XX:MaxGCPauseMillis ) |
碎片化问题 | 可能严重,影响 Full GC 频率 | 通过 Region 避免碎片 |
吞吐量 | 较高,但 Full GC 影响较大 | 较稳定,整体吞吐量较优 |
Full GC 触发 | 碎片化严重时容易触发 | 极少发生 |
5.5 GC不仅会对堆进行GC还会对方法区GC
-
堆(Heap)GC:
- 主要回收 Java 对象(实例)。
- 频繁触发 GC(Minor GC、Major GC、Full GC)。
-
方法区(Method Area)GC:
- 主要回收 类的元数据、常量池、JIT 编译后的代码缓存。
- 较少触发 GC(通常在类卸载时进行)。
方法区 GC 主要回收哪些内容?
(1)废弃的常量
- 字符串常量池(String Pool)
- 运行时常量池中的数值、类名、方法名、字段名
(2)无用的类
类的卸载(Class Unloading) 发生在以下条件都满足时:
- 该类的所有实例都被 GC 回收(即堆中不再存在该类的对象)。
- 加载该类的 ClassLoader 本身已经被回收。
- 该类没有被静态变量(static)引用。
注意:JVM 默认不会主动卸载类,通常只有在动态加载和卸载 ClassLoader 时才会发生(如 Web 服务器动态部署)。
(3)JIT 编译缓存
JVM 的 JIT 编译器(Just-In-Time Compiler) 会将热点代码编译成本地机器码并缓存到 代码缓存(Code Cache)。当缓存空间不足时,JVM 可能会触发 GC 清除不常用的编译代码。
方法区 GC 触发时机
- 动态代理、反射、CGLIB 生成的类较多时(如 Spring 框架)。
- 大量的字符串常量、方法名、字段名 存入常量池。
- 频繁卸载 ClassLoader(如 Web 服务器重新加载 WAR 包)。
- JIT 编译器缓存过多代码(如长时间运行的大型 Java 程序)。
6. JVM 内存泄漏
6.1 什么是 JVM 内存泄漏?
在 JVM 中,内存泄漏(Memory Leak) 指的是程序运行过程中,不再使用的对象仍然被引用,导致 GC 无法回收它们,进而导致堆内存(Heap)不断膨胀,最终可能触发 OutOfMemoryError(OOM)。
尽管 Java 有 垃圾回收机制(GC),但如果对象仍然被可达引用(Reachable),即使程序不再使用它们,GC 也不会回收这些对象。这就形成了内存泄漏。
6.2 内存泄漏的表现
1. 堆内存持续增长
- JVM 运行时间越长,内存占用越高,甚至 OOM
- GC 频率升高,但老年代(Old Generation)对象无法释放
2. 应用性能下降
- 内存占用增加,导致频繁 GC
- 应用响应时间变慢,甚至崩溃
3. OutOfMemoryError: Java heap space
- 堆空间耗尽,程序崩溃
- 发生在 大量对象未释放 或 大对象占用过多内存 的情况下。
6.3 JVM 内存泄漏的常见原因
Java 内存泄漏的根本原因是 无用对象仍然被引用,GC 无法回收它们。常见的几种情况如下:
6.3.1 静态集合类 / 静态变量导致的内存泄漏
原因
- 静态变量(static)属于类,生命周期与 JVM 一致,不会被 GC 自动回收。
- 若静态变量持有大量对象引用,即使对象本身不再使用,也不会被回收,从而造成 堆积。
示例
import java.util.*;
public class StaticCollectionLeak {
private static final List<byte[]> memoryLeakList = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
byte[] largeObject = new byte[1024 * 1024]; // 1MB
memoryLeakList.add(largeObject);
}
}
}
问题:memoryLeakList
是 static
,导致所有对象即使不再需要,仍然不会被 GC 回收。
解决方案
- 避免静态集合存储大量对象
- 使用
WeakReference
或SoftReference
- 手动调用集合的clear()方法清理
6.3.2 监听器 & 观察者模式导致的泄漏
原因
- 监听器(Listener)或观察者模式(Observer)会使对象之间形成强引用,即使对象不再使用,监听器仍然会保持对它的引用,导致 GC 无法回收。
示例
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
}
interface EventListener {
void onEvent();
}
public class ListenerLeak {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
EventListener listener = () -> System.out.println("Event received!");
eventSource.addListener(listener);
}
}
问题:
listeners
集合会一直持有EventListener
对象的引用,即使它们不再被使用,导致 GC 不能回收它们。
解决方案:
- 使用
WeakReference
弱引用:
private final List<WeakReference<EventListener>> listeners = new ArrayList<>();
6.3.3 线程本地变量(ThreadLocal)泄漏
原因
ThreadLocal
绑定的变量存储在线程的ThreadLocalMap
中,但如果不手动清理,线程池复用线程时可能会导致数据泄漏。
示例
public class ThreadLocalLeak {
private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
threadLocal.set(new byte[10 * 1024 * 1024]); // 10MB
});
thread.start();
}
}
问题:线程执行完后,ThreadLocal
变量没有被清理,导致占用 10MB 内存无法释放。
解决方案:
- 在
finally
语句中手动清理ThreadLocal
变量:
try {
threadLocal.set(new byte[10 * 1024 * 1024]);
} finally {
threadLocal.remove();
}
6.3.4 内部类 & 匿名类导致的泄漏
原因
- 非静态内部类 或 匿名类 持有对外部类的隐式引用,如果外部类仍然存活,内部类不会被 GC 回收。
示例
public class InnerClassLeak {
private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB
public void createAnonymousClass() {
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(largeArray.length);
}
};
new Thread(task).start();
}
public static void main(String[] args) {
new InnerClassLeak().createAnonymousClass();
}
}
问题:
task
作为匿名内部类会持有 InnerClassLeak
的引用?
在 Java 中,非静态内部类和匿名类会隐式地持有外部类实例的引用,这就是 隐式引用(Implicit Reference)。这意味着:
- 在 匿名内部类
task
的run()
方法内部,访问了largeArray
(InnerClassLeak
的成员变量)。 - 由于
largeArray
是InnerClassLeak
的实例变量,所以 匿名类task
需要持有InnerClassLeak
的引用,才能访问largeArray
。 - 这种 外部类的隐式引用 可能会导致
InnerClassLeak
对象无法被 GC 回收,从而导致内存泄漏。
解决方案:
- 使用静态内部类,避免隐式引用:
public class InnerClassLeak {
private byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB
public void createStaticInnerClass() {
new Thread(new StaticTask(largeArray)).start(); // 直接传递 largeArray
}
// 变为静态内部类
private static class StaticTask implements Runnable {
private final byte[] arrayRef;
StaticTask(byte[] arrayRef) {
this.arrayRef = arrayRef;
}
@Override
public void run() {
System.out.println(arrayRef.length); // 访问传入的参数,而不是外部类变量
}
}
public static void main(String[] args) {
new InnerClassLeak().createStaticInnerClass();
}
}
为什么静态内部类可以避免内存泄漏?
-
静态内部类不会持有外部类
InnerClassLeak
的隐式引用:StaticTask
使用static
修饰后,就不再与InnerClassLeak
绑定,它变成了独立的类。StaticTask
不会自动持有InnerClassLeak
的实例引用。
-
显式传递
largeArray
,避免隐式引用new Thread(new StaticTask(largeArray)).start();
- 我们显式地将
largeArray
传递给StaticTask
构造方法,这样StaticTask
只持有largeArray
的引用,而不是InnerClassLeak
的整个实例。 - 即使
InnerClassLeak
被 GC 回收,StaticTask
仍然可以正常运行。
- 我们显式地将
方案 是否会导致内存泄漏? 原因 匿名内部类 ✅ 可能会泄漏 持有外部类 InnerClassLeak
的隐式引用,导致largeArray
无法回收静态内部类 ❌ 不会泄漏 不再持有 InnerClassLeak
的引用,只持有largeArray
,可以安全回收最佳实践
- 避免匿名类访问外部类的实例变量,否则可能会无意间创建隐式引用,导致对象不能被 GC。
- 如果必须使用内部类,建议使用
static
内部类,并通过构造方法传递所需数据,避免隐式引用外部类。
6.3.5 数据库连接 / IO 资源未关闭
原因
- 数据库连接(JDBC)、文件流、Socket 连接未正确关闭,导致资源泄漏,最终耗尽可用内存。
- GC 无法自动回收这些外部资源。
示例
public class ConnectionLeak {
public static void main(String[] args) throws Exception {
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
// 🚨 资源未关闭,泄漏
}
}
解决方案
- 使用
try-with-resources
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users")) { while (rs.next()) { System.out.println(rs.getString("name")); } }
try-with-resources
确保所有资源自动关闭!
6.4 如何检测 JVM 内存泄漏?
(1) 使用 jmap
查看堆内存
jmap -histo:live <pid>
- 分析大对象占用情况,找出无法被回收的对象。
(2)使用 jconsole
监控 JVM 内存
- 实时观察堆内存使用趋势,发现是否存在不断增长且无法释放的对象。
(3)使用 VisualVM
进行 Heap Dump 分析
jmap -dump:format=b,file=heapdump.hprof <pid>
- 导入
VisualVM
,分析对象引用关系,找出无法被 GC 回收的对象。
(4)使用 MAT(Memory Analyzer Tool)
- MAT(Eclipse Memory Analyzer) 可以分析
.hprof
文件,找出GC Root 保持的对象,定位泄漏点。
7. JVM 内存溢出(OutOfMemoryError, OOM)
7.1. 什么是 JVM 内存溢出?
JVM 内存溢出(OutOfMemoryError,简称 OOM) 是指 JVM 试图分配内存,但由于内存不足或内存无法回收,导致 JVM 运行失败并抛出 java.lang.OutOfMemoryError
异常。
JVM 主要的内存区域:
- 堆(Heap):存储对象实例。
- 栈(Stack):存储方法调用帧、局部变量。
- 方法区(Method Area)(JDK 8+ 称为 Metaspace):存储类元数据、方法、静态变量等。
- 直接内存(Direct Memory):NIO 直接分配的操作系统内存。
7.2. 常见的 OOM 类型
JVM 内存溢出 通常发生在以下几种区域:
OOM 错误类型 | 发生区域 | 主要原因 |
---|---|---|
java.lang.OutOfMemoryError: Java heap space | 堆(Heap) | - 对象过多,无法回收,导致堆空间耗尽(如集合无限增长,缓存未清理)。 - 单个大对象分配失败(如一次性分配超大数组)。 |
java.lang.OutOfMemoryError: GC overhead limit exceeded | 堆(Heap) | - GC 频繁运行但每次回收内存极少,导致 CPU 资源被大量消耗。 |
java.lang.OutOfMemoryError: Metaspace | 方法区(Metaspace) | - 类加载过多(如 Spring 频繁创建代理类,动态类加载)。 - 类无法卸载(如自定义 ClassLoader 造成内存泄漏)。 |
java.lang.StackOverflowError | 栈(Stack) | - 方法递归过深,导致栈帧溢出(如无限递归)。 - 每个线程栈空间不足,导致溢出。 |
java.lang.OutOfMemoryError: unable to create new native thread | 本地内存(OS 线程数) | - 线程创建过多,超出 OS 允许的最大线程数(如无限创建 new Thread() )。- 每个线程栈大小过大,导致系统无法分配新线程。 |
java.lang.OutOfMemoryError: Direct buffer memory | 直接内存(Direct Memory) | - NIO 直接缓冲区 (ByteBuffer.allocateDirect() ) 分配过多,超过 MaxDirectMemorySize 限制。 |
java.lang.OutOfMemoryError: Swap space | 操作系统 Swap 交换空间 | - 应用分配内存过多,导致 OS 交换空间耗尽(一般在物理内存不足时发生)。 |
java.lang.OutOfMemoryError: Requested array size exceeds VM limit | 堆(Heap) | - 试图分配超大数组(如 new int[Integer.MAX_VALUE] )。 |
java.lang.OutOfMemoryError: Compressed class space | 方法区(Metaspace, Class Space) | - JVM 运行时加载类过多,超出 CompressedClassSpaceSize 限制。 |
7.3 各种 JVM 内存溢出情况
7.3.1. 堆内存溢出(java.lang.OutOfMemoryError: Java heap space)
原因
- 创建对象过多,堆空间不断增长,无法回收(如:缓存未清理、集合不断增长)。
- 单个大对象分配失败(如:一次性分配一个超大数组)。
- 内存泄漏,无用对象仍然被引用,GC 无法清理。
示例
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[10 * 1024 * 1024]); // 每次分配 10MB
}
}
}
解决方案
✅ 增大堆空间(适用于对象确实需要更多内存的情况):
java -Xms2g -Xmx4g HeapOOM
✅ 优化 GC 策略
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 HeapOOM
✅ 检测内存泄漏
- 使用
jmap
jmap -histo:live <pid>
- 使用 Heap Dump
jmap -dump:format=b,file=heapdump.hprof <pid>
- 使用
VisualVM
或MAT
(Memory Analyzer Tool)分析heapdump.hprof
7.3.2. 栈内存溢出(StackOverflowError 或 Stack Space OOM)
原因
- 递归调用过深,导致栈帧不断压入,最终超过栈空间大小。
- 创建大量线程,导致 JVM 线程栈空间耗尽。
示例 1:递归调用导致 StackOverflowError
public class StackOverflowDemo {
public void recursiveMethod() {
recursiveMethod(); // 无限递归
}
public static void main(String[] args) {
new StackOverflowDemo().recursiveMethod();
}
}
示例 2:创建大量线程导致 OutOfMemoryError: Unable to create new native thread
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadOOM {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1000);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
while (true) {} // 每个线程执行无限循环
});
}
}
}
解决方案
✅ 减少递归深度 ✅ 增大栈空间
java -Xss1m StackOverflowDemo
✅ 控制线程池大小
ExecutorService executor = Executors.newFixedThreadPool(100);
7.3.3. 方法区/元空间溢出(Metaspace OOM)
原因
- 大量动态生成的类(如:大量使用
CGLIB
、Javassist
动态代理)。 - Spring Boot 等框架频繁加载新类,导致
Metaspace
过满。 - 应用长时间运行,但类卸载不及时,导致
Metaspace
持续增长。
示例
import javassist.ClassPool;
public class MetaspaceOOM {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
classPool.makeClass("com.example.GeneratedClass" + i).toClass();
}
}
}
解决方案
✅ 增加 Metaspace
大小
java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m MetaspaceOOM
✅ 减少动态生成的类
7.3.4. GC 过载导致 OOM(java.lang.OutOfMemoryError: GC Overhead limit exceeded)
原因
- GC 运行时间过长,超过 98% CPU,但回收的内存不足 2%,JVM 触发此 OOM 保护机制。
- 堆内存不足,导致 GC 频繁执行,但对象回收效果不佳。
示例
import java.util.HashMap;
import java.util.Map;
public class GCOverheadOOM {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
int i = 0;
while (true) {
map.put(i, "OOM Test " + i++); // 不断填充 HashMap
}
}
}
解决方案
✅ 增大堆空间,减少 GC 触发
java -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 GCOverheadOOM
✅ 使用 -XX:-UseGCOverheadLimit
关闭 GC 限制
java -Xmx4g -XX:-UseGCOverheadLimit GCOverheadOOM
7.3.5. 直接内存溢出(Direct Buffer Memory OOM)
原因
NIO ByteBuffer
分配过多,导致 Direct Memory 耗尽。- JVM 直接内存上限太低,无法满足
ByteBuffer.allocateDirect()
分配请求。
示例
import java.nio.ByteBuffer;
public class DirectMemoryOOM {
public static void main(String[] args) {
while (true) {
ByteBuffer.allocateDirect(1024 * 1024); // 每次申请 1MB 直接内存
}
}
}
解决方案
✅ 增大直接内存
java -XX:MaxDirectMemorySize=512m DirectMemoryOOM
✅ 避免无限制分配
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
buffer.clear(); // 复用 Buffer,避免反复分配
8. JVM 常见参数及其作用
在 JVM 运行 Java 应用时,我们可以使用 JVM 参数 来控制内存分配、垃圾回收(GC)策略、性能优化等。本文将详细介绍 JVM 常见参数的分类、作用、以及如何设置这些参数。
8.1 JVM 参数设置方法
在生产环境中,JVM 参数通常通过以下方式进行设置:
(1)直接通过 java
命令行设置
适用于 独立 Java 应用、测试环境,例如:
java -Xms2g -Xmx4g -XX:+UseG1GC -jar myapp.jar
(2)在 JAVA_OPTS
或 JAVA_TOOL_OPTIONS
环境变量中设置
适用于 Web 服务器(Tomcat、Spring Boot、微服务):
export JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC"
(3)在 Docker 容器中设置
容器化部署时,一般通过环境变量 JAVA_OPTS
传递:
docker run -e "JAVA_OPTS=-Xmx4g -XX:+UseG1GC" my-java-app
(4)在 Kubernetes(K8s)中设置
对于 K8s 部署的 Java 应用,可以在 Deployment
配置文件中设置:
env:
- name: JAVA_OPTS
value: "-Xms2g -Xmx4g -XX:+UseG1GC"
8.2 常用 JVM 参数及生产环境实践
8.2.1 内存管理参数
作用:控制 JVM 的 堆(Heap)、栈(Stack)、方法区(Metaspace) 大小,影响 GC 频率和性能。
参数 | 作用 | 生产环境建议 |
---|---|---|
-Xms<size> | 初始堆大小(默认 1/64 物理内存) | 设置与 -Xmx 相同,避免运行时扩展 |
-Xmx<size> | 最大堆大小(默认 1/4 物理内存) | 根据可用内存大小设置,如 -Xmx4g |
-XX:NewRatio=n | 新生代:老年代 比例(默认 2 ,即 1:2 ) | 推荐 NewRatio=2 ,适用于吞吐量型应用 |
-XX:SurvivorRatio=n | Eden:Survivor 比例(默认 8:1:1 ) | 保持默认 SurvivorRatio=8 |
-Xss<size> | 每个线程的栈大小(默认 1MB) | 适用于高并发应用,如 -Xss512k 减少栈内存占用 |
-XX:MetaspaceSize=256m | JDK 8+ 方法区大小 | 推荐 256m |
-XX:MaxMetaspaceSize=512m | 元空间最大值 | 防止 Metaspace OOM ,推荐 512m |
8.2.2 GC(垃圾回收)策略
作用:选择合适的 GC 机制,降低 STW(Stop-The-World)
停顿时间,提高吞吐量。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:+UseSerialGC | 单线程 GC(适用于小型应用) | 不推荐用于生产环境 |
-XX:+UseParallelGC | 多线程吞吐量 GC | 适用于批处理任务、Kafka、Spark |
-XX:+UseG1GC | 低延迟 GC(默认) | 适用于 Web 服务器 / 微服务 |
-XX:+UseZGC | 超低延迟 GC(JDK 11+) | 适用于金融、超大堆(TB 级)应用 |
-XX:MaxGCPauseMillis=200 | 最大 GC 停顿时间 | 适用于 G1 GC,控制 STW 时长 |
8.2.3 JIT(Just-In-Time 编译)优化
作用:优化 JIT 编译,提高 Java 代码执行性能。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:+TieredCompilation | 分层 JIT 编译 | 默认启用,适用于高并发应用 |
-XX:+PrintCompilation | 打印 JIT 编译方法 | 调试时启用 |
8.2.4 线程管理
作用:控制并发线程数,提高 CPU 资源利用率。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:ParallelGCThreads=<n> | GC 并行线程数 | 推荐 CPU 核心数 / 2 |
-XX:ConcGCThreads=<n> | G1 / ZGC 并发线程数 | 适用于低延迟应用 |
8.2.5 监控与日志
作用:启用 GC 日志,监控应用运行状态。
参数 | 作用 | 生产环境建议 |
---|---|---|
-XX:+HeapDumpOnOutOfMemoryError | OOM 生成 Heap Dump | 强烈建议启用 |
-XX:HeapDumpPath=<path> | Heap Dump 存储路径 | 推荐 /var/logs/heapdump.hprof |
-XX:+PrintGCDetails | 打印 GC 详情 | 生产环境推荐 |
-Xloggc:/var/logs/gc.log | GC 日志存储路径 | 用于 GC 监控分析 |