一、JVM 整体架构
JVM 本质与功能
Java 虚拟机(JVM)本质上是一款跨平台的虚拟计算机,它严格遵循 Oracle 发布的《Java 虚拟机规范》,构成了 Java 技术体系的核心。JVM 的主要职责是将编译生成的字节码(.class 文件)翻译成特定平台的机器码并执行,从而实现 Java 著名的"一次编写,到处运行"(Write Once, Run Anywhere)特性。值得注意的是,JVM 不仅支持 Java 语言,任何能编译成有效字节码的语言(如 Kotlin、Scala)都能在 JVM 上运行。
整体架构
JVM 的架构设计精巧,可分为五大核心模块协同工作:
1.1 架构总览图
┌─────────────────────────────────────────────────────────────┐
│ 类加载子系统 (ClassLoader) │
├─────────────────────────────────────────────────────────────┤
│ 运行时数据区 (Runtime Data Area) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 方法区 │ │ 堆内存 │ │ 虚拟机栈 │ │本地方法栈 │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 程序计数器 (Program Counter Register) │ │
│ └───────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 执行引擎 (Execution Engine) │
├─────────────────────────────────────────────────────────────┤
│ 本地方法接口 (JNI) │
├─────────────────────────────────────────────────────────────┤
│ 垃圾回收器 (GC) │
└─────────────────────────────────────────────────────────────┘
1.2 各模块核心作用
-
类加载子系统:
- 负责将.class 文件加载到内存中
- 遵循"双亲委派"机制,依次通过启动类加载器、扩展类加载器和应用类加载器进行加载
- 完成"加载、验证、准备、解析、初始化"5 个阶段的生命周期
- 最终生成可被 JVM 使用的 Class 对象
- 示例:加载一个User类时,会先检查其父类是否已加载
-
运行时数据区:
- JVM 的内存核心区域,包含:
- 方法区:存储类信息、常量、静态变量等(JDK8后称为元空间)
- 堆内存:对象实例的主要存储区域(分为新生代和老年代)
- 虚拟机栈:线程私有,存储方法调用栈帧(包含局部变量表、操作数栈等)
- 本地方法栈:服务于本地方法调用
- 程序计数器:记录当前线程执行到的字节码位置
- 所有 Java 程序的运行都依赖此区域的内存分配与回收
- JVM 的内存核心区域,包含:
-
执行引擎:
- 将字节码翻译成机器码并执行
- 主要实现方式:
- 解释器:逐行翻译字节码,启动速度快但执行效率较低
- 即时编译器(JIT):将热点代码(HotSpot)编译为本地机器码,执行效率高
- 现代JVM(如HotSpot)通常采用解释器和JIT混合模式
-
本地方法接口(JNI):
- 提供Java调用本地方法(如C/C++)的能力
- 常用于:
- 操作硬件设备(如通过Java调用底层驱动)
- 性能敏感场景(如图形处理、加密算法)
- 复用现有本地库
- 示例:Java标准库中的File类部分方法就是通过JNI实现的
-
垃圾回收器(GC):
- 自动回收堆内存中"不再被引用"的对象
- 采用多种算法(如标记-清除、复制、标记-整理等)
- 主流实现包括:
- 串行收集器(Serial GC)
- 并行收集器(Parallel GC)
- CMS收集器
- G1收集器
- ZGC(低延迟垃圾收集器)
- 可显著减轻开发者手动管理内存的负担,避免内存泄漏
- 示例:当对象失去所有引用时,GC会在适当时候回收其占用的内存
二、运行时数据区
2.1 线程共享区域
2.1.1 堆(Heap):对象存储的"主战场"
作用:堆是Java内存管理的核心区域,存储所有通过new关键字创建的对象实例和数组。它是JVM内存中最大的区域,也是垃圾回收(GC)的主要工作区域。堆的大小直接决定了JVM能够创建和管理多少对象。
内存划分:现代JVM采用分代收集算法优化GC效率,堆被划分为两个主要部分:
-
新生代(Young Generation):
- 占堆内存的1/3(默认)
- 存储新创建的对象,生命周期通常很短
- 进一步划分为:
- Eden区(80%):对象首次分配的区域
- Survivor0区(10%):Minor GC后存活的对象
- Survivor1区(10%):用于对象复制
- 发生Minor GC时,存活对象在Survivor区之间复制,达到一定年龄(默认15次)后晋升到老年代
-
老年代(Old Generation):
- 占堆内存的2/3(默认)
- 存储长期存活的对象
- 发生Major GC/Full GC时清理
关键参数:
-Xms和-Xmx:设置堆初始和最大内存(如-Xms2g -Xmx4g)-XX:NewRatio:调整新生代与老年代比例(默认2)-XX:SurvivorRatio:设置Eden与Survivor区比例(默认8)-Xmn:直接设置新生代大小(如-Xmn1g)
性能调优:
- 避免频繁Full GC:合理设置新生代大小
- 减少对象晋升:适当增加Survivor区
- 避免内存泄漏:监控对象生命周期
2.1.2 方法区(Method Area):类信息的"仓库"
作用:方法区存储JVM加载的类元数据,包括:
- 类结构信息(名称、修饰符、父类、接口)
- 字段和方法信息
- 运行时常量池
- 静态变量
- JIT编译后的代码
实现演变:
-
JDK 1.7及之前:
- 使用"永久代"(PermGen)实现
- 大小受限(
-XX:PermSize和-XX:MaxPermSize) - 常见问题:字符串常量池导致OOM
-
JDK 1.8及之后:
- 使用"元空间"(Metaspace)替代永久代
- 使用本地内存而非JVM内存
- 自动扩展(受
-XX:MetaspaceSize和-XX:MaxMetaspaceSize限制) - 优点:减少OOM风险,支持动态扩展
常见问题排查:
- 类加载过多:检查动态代理、反射使用
- 元数据泄漏:监控Metaspace增长
- 调优建议:设置合理的初始大小
2.2 线程私有区域
2.2.1 虚拟机栈(VM Stack):方法执行的"调用栈"
结构:每个线程拥有独立的虚拟机栈,栈由多个栈帧(Stack Frame)组成,每个方法调用创建一个栈帧。
栈帧组成:
-
局部变量表:
- 存储方法参数和局部变量
- 基本类型直接存储值
- 引用类型存储引用地址
- 大小在编译期确定
-
操作数栈:
- 用于方法执行时的计算
- 存储中间计算结果
-
动态链接:
- 指向运行时常量池的方法引用
-
方法返回地址:
- 记录方法执行完成后的返回位置
异常处理:
StackOverflowError:典型场景是无限递归OOM:线程创建过多导致栈内存耗尽
调优参数:
-Xss:设置线程栈大小(如-Xss1m)
2.2.2 本地方法栈(Native Method Stack)
特点:
- 服务于JNI调用的本地方法
- 在HotSpot中与虚拟机栈合并
- 不同JVM实现差异较大
- 同样可能出现StackOverflowError和OOM
2.2.3 程序计数器(Program Counter Register):线程执行的"导航仪"
详细功能:
- 每个线程独立拥有
- 记录当前线程执行的字节码指令地址
- 线程切换时保存执行状态
- 执行本地方法时值为undefined
- 唯一不会出现OOM的区域
- 对程序性能无直接影响
实现特点:
- 占用空间极小
- 快速访问
- 线程私有,无需同步
- JVM内部实现细节,开发者不可见
三、类加载机制
3.1 类加载的5个阶段
3.1.1 加载(Loading):找到并读取.class文件
详细过程:
-
查找.class文件:JVM根据类的全限定名(如
com.example.User)查找对应的.class文件,查找路径包括:- 本地文件系统
- JAR/ZIP包
- 网络资源(如Applet)
- 运行时生成的类(动态代理)
- 其他自定义来源(如数据库)
-
读取字节流:将.class文件转换为二进制字节流,这个过程可能涉及:
- 文件IO操作
- 网络传输
- 解密操作(如加密的类文件)
-
创建Class对象:在堆中生成
java.lang.Class对象,作为类元数据的访问入口。这个对象包含:- 类名
- 修饰符
- 父类信息
- 实现的接口
- 方法信息
- 字段信息
3.1.2 验证(Verification):确保.class文件合法
详细验证步骤:
-
文件格式验证:
- 检查魔数是否为0xCAFEBABE
- 检查主次版本号是否在当前JVM支持范围内
- 检查常量池中的常量是否有不被支持的类型
- 检查指向常量的索引是否指向了不存在的常量
-
元数据验证:
- 检查类是否有父类(除Object外)
- 检查父类是否允许继承(如final类)
- 检查字段/方法是否与父类冲突(如覆盖final方法)
- 检查类是否实现了所有抽象方法
-
字节码验证(最复杂):
- 检查操作数栈类型与指令是否匹配
- 检查跳转指令是否指向合法位置
- 检查类型转换是否合法
- 检查方法调用是否匹配参数类型
-
符号引用验证:
- 检查引用的类能否被找到
- 检查字段/方法是否存在于对应类中
- 检查访问权限是否允许(如private方法)
3.1.3 准备(Preparation):为静态变量分配内存并赋默认值
详细说明:
-
内存分配:
- 静态变量存储在方法区
- 为每个静态变量分配内存空间
- 基本类型分配固定大小空间(如int=4字节)
- 引用类型分配指针大小空间
-
默认值赋值:
- int/long/char/short/byte:0
- float/double:0.0
- boolean:false
- 引用类型:null
-
特殊处理:
- 常量(final static)会在准备阶段直接赋值
- 非final的static变量维持默认值,直到初始化阶段
3.1.4 解析(Resolution):将符号引用转为直接引用
详细解析过程:
-
类/接口解析:
- 检查符号引用的全限定名
- 检查访问权限
- 加载引用的类(如果未加载)
- 返回直接引用(类元数据指针)
-
字段解析:
- 查找字段所属的类
- 检查字段是否存在
- 检查访问权限
- 返回字段偏移量或访问方法
-
方法解析:
- 查找方法所属的类
- 检查方法是否存在
- 检查访问权限
- 返回方法入口地址
-
接口方法解析:
- 查找接口方法
- 检查实现类是否实现了该方法
- 返回方法入口地址
3.1.5 初始化(Initialization):执行静态代码块和静态变量赋值
详细初始化过程:
-
触发条件(主动使用):
- new关键字创建实例
- 调用静态方法
- 访问静态字段(非final)
- 反射调用(Class.forName)
- 初始化子类(会先初始化父类)
- 包含main()方法的启动类
-
初始化顺序:
- 父类先于子类初始化
- 静态变量和静态代码块按代码顺序执行
- 静态代码块只执行一次
-
线程安全:
- 初始化过程是线程安全的
- JVM会加锁确保只有一个线程执行初始化
- 其他线程会阻塞等待初始化完成
3.2 类加载器体系
3.2.1 三层默认类加载器
扩展说明:
-
启动类加载器(Bootstrap ClassLoader):
- 加载路径:
$JAVA_HOME/jre/lib目录下的核心库 - 特有特性:是唯一没有父加载器的加载器
- 识别方式:
String.class.getClassLoader()返回null
- 加载路径:
-
扩展类加载器(Extension ClassLoader):
- 加载路径:
$JAVA_HOME/jre/lib/ext目录 - 父加载器:启动类加载器
- 可配置:通过
-Djava.ext.dirs修改加载路径
- 加载路径:
-
应用程序类加载器(Application ClassLoader):
- 加载路径:classpath指定的所有路径
- 父加载器:扩展类加载器
- 默认加载器:
Thread.currentThread().getContextClassLoader()通常返回此加载器
3.2.2 双亲委派模型
工作机制详解:
-
委派流程:
- 类加载请求首先交给当前类加载器的缓存检查
- 缓存未命中,则委派给父加载器
- 父加载器递归执行相同逻辑
- 所有父加载器都无法加载时,才由当前加载器尝试加载
-
代码实现:
protected Class<?> loadClass(String name, boolean resolve) { synchronized (getClassLoadingLock(name)) { // 1. 检查是否已加载 Class<?> c = findLoadedClass(name); if (c == null) { try { // 2. 父加载器不为null则委派给父加载器 if (parent != null) { c = parent.loadClass(name, false); } else { // 3. 父加载器为null则委派给启动类加载器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {} // 4. 父加载器都找不到时自己加载 if (c == null) { c = findClass(name); } } return c; } } -
破坏双亲委派:
- 历史案例:JDBC SPI(Service Provider Interface)
- 原因:基础类需要回调用户代码
- 解决方案:使用线程上下文类加载器
- 现代方式:模块化系统(Java 9+)提供了更灵活的机制
四、垃圾回收
GC 概述
GC(Garbage Collection)是 JVM 自动内存管理的核心机制,主要负责回收堆内存中"不再被引用"的对象。要深入理解 GC,需要明确以下三个核心问题:
- 哪些对象需要回收(对象存活判定)
- 如何回收(垃圾回收算法)
- 何时回收(GC 触发时机)
4.1 如何判断对象"已死亡"?
GC 的第一步是识别"无用对象",主流判断算法有两种:
4.1.1 引用计数法(Reference Counting)
原理:为每个对象维护一个"引用计数器",当对象被引用时计数器 +1,引用失效时计数器 -1;当计数器为 0 时,认为对象可回收。
具体实现:
- 创建对象时初始化计数器为 1
- 当对象被赋值给变量时计数器 +1
- 当引用超出作用域或被置为 null 时计数器 -1
示例:
Object objA = new Object(); // objA 引用计数=1
Object objB = objA; // objA 引用计数=2
objB = null; // objA 引用计数=1
objA = null; // objA 引用计数=0(可回收)
缺点:
- 无法解决循环引用问题:如 A 引用 B,B 引用 A,两者计数器均为 1,但均无其他外部引用,无法被回收
- 性能开销:每次引用变更都需要更新计数器
- 线程安全问题:多线程环境下需要同步操作计数器
因此 JVM 未采用此算法,而是使用更为可靠的可达性分析算法。
4.1.2 可达性分析算法(Reachability Analysis)
原理:以"GC Roots"为起点,向下遍历引用链(对象间的引用关系),若某个对象无法通过任何 GC Roots 到达(引用链断裂),则认为该对象可回收。
算法流程:
- 枚举所有 GC Roots 对象
- 从这些根对象开始,递归遍历所有引用关系
- 标记所有被访问到的对象为存活
- 未被标记的对象即为可回收对象
GC Roots 的常见来源:
- 虚拟机栈中局部变量表引用的对象:如当前正在执行的方法中的局部变量
void method() { Object localObj = new Object(); // localObj 是 GC Root } - 方法区中静态变量和常量引用的对象:
static Object staticObj = new Object(); // staticObj 是 GC Root - 本地方法栈中本地方法引用的对象
- JVM 内部的引用:如类加载器、GC 线程等
- 同步锁持有的对象(synchronized 关键字关联的对象)
示例分析:
class User {
private Address address;
// getter/setter...
}
User user = new User(); // user 是 GC Root
user.setAddress(new Address());
user = null; // 原先的 User 和 Address 对象都不可达
对象引用强度分类:
- 强引用(Strong Reference):普通对象引用,不会被 GC
- 软引用(Soft Reference):内存不足时会被回收
- 弱引用(Weak Reference):GC 时立即被回收
- 虚引用(Phantom Reference):无法通过引用获取对象,用于跟踪对象被回收的状态
4.2 常见垃圾回收算法
识别出无用对象后,GC 需要通过算法将其回收并释放内存,主流算法包括:
4.2.1 标记 - 清除算法(Mark-Sweep)
步骤:
- 标记阶段:通过可达性分析标记所有可回收对象
- 遍历所有 GC Roots,标记可达对象
- 通常使用位图或对象头标记
- 清除阶段:遍历堆内存,直接回收标记对象的内存空间
- 将空闲内存记录到空闲列表
- 新对象分配时从空闲列表查找合适空间
优点:
- 实现简单
- 不需要移动对象(适用于存活对象多的情况)
缺点:
- 内存碎片化严重:回收后的内存块大小不一,可能导致无法分配大对象
- 示例:堆中有 100MB 空闲内存,但分散为 10 个 10MB 的块,无法分配 20MB 的对象
- 效率问题:
- 标记和清除都需要遍历全堆
- 随着堆增大,GC 时间线性增长
应用场景:
- 老年代回收(配合其他算法使用)
- CMS 收集器的并发清除阶段
4.2.2 复制算法(Copying)
步骤:
- 将堆内存分为两个大小相等的区域(如新生代的 Eden 区和 Survivor 区)
- 只使用其中一个区域(如 Eden+S0)分配对象
- 当该区域内存满时:
- 标记存活对象
- 将存活对象按顺序复制到另一个空闲区域(S1)
- 清空原区域的所有对象
- 角色互换:将 S1 作为新的使用区域
内存布局:
新生代典型划分:
+--------+---------+---------+
| Eden | From(S0)| To(S1) |
+--------+---------+---------+
(默认比例:Eden:S0:S1 = 8:1:1)
优点:
- 无内存碎片化:存活对象连续存储
- 效率高:
- 只需复制存活对象
- 不需要遍历全堆
- 分配简单:使用指针碰撞(bump-the-pointer)快速分配
缺点:
- 内存利用率低:仅使用一半内存
- 存活对象不能太多:适合新生代(98%的对象朝生夕死)
- 需要额外空间处理担保失败(当存活对象超过To区容量时)
优化:
- 多 Survivor 区(如 S0、S1)
- 动态调整 Survivor 区大小
- 结合逃逸分析优化复制策略
应用场景:
- 新生代回收(Minor GC)
- Serial、ParNew、G1 等收集器的新生代回收
4.2.3 标记 - 整理算法(Mark-Compact)
步骤:
- 标记阶段:与标记-清除算法一致,标记可回收对象
- 整理阶段:
- 将存活对象向堆内存的一端移动
- 更新所有引用这些对象的指针
- 直接清空另一端的所有可回收对象
内存变化示例:
回收前:
[存活][垃圾][存活][垃圾][存活]
回收后:
[存活][存活][存活][空闲][空闲]
优点:
- 无内存碎片化
- 内存利用率高(优于复制算法)
- 适合存活对象多的情况
缺点:
- 效率低:
- 需要移动存活对象
- 需要更新所有引用
- STW 时间长:不适合对延迟敏感的应用
优化技术:
- 增量整理
- 并行整理
- 滑动整理 vs 压缩整理
应用场景:
- 老年代回收(Full GC)
- Serial Old、Parallel Old 收集器
- G1 的老年区回收
4.3 主流垃圾收集器
垃圾收集器是 GC 算法的具体实现,不同收集器适用于不同场景。HotSpot 虚拟机提供了多种收集器,常见组合如下:
| 收集器组合 | 适用区域 | 核心特点 | 适用场景 |
|---|---|---|---|
| Serial + Serial Old | 新生代 + 老年代 | 单线程收集,STW | 客户端应用、小内存环境 |
| ParNew + CMS | 新生代 + 老年代 | 多线程并发收集,低延迟 | 服务端应用,对延迟敏感 |
| Parallel Scavenge + Parallel Old | 新生代 + 老年代 | 多线程并行收集,高吞吐量 | 后台计算型应用 |
| G1(Garbage-First) | 全堆 | 分区收集,可预测停顿 | 大堆内存,平衡吞吐和延迟 |
| ZGC | 全堆 | 并发收集,超低延迟(<10ms) | 超大堆内存,极致延迟要求 |
| Shenandoah | 全堆 | 并发收集,低延迟 | 与 ZGC 类似 |
4.3.1 各收集器详细解析
(1)Serial 收集器(新生代)
工作模式:
- 单线程收集
- GC 时会暂停所有用户线程(STW,Stop The World)
- 采用复制算法
实现原理:
- 暂停所有应用线程
- 从 GC Roots 开始标记存活对象
- 将 Eden 区和 S0 区的存活对象复制到 S1 区
- 清空 Eden 和 S0 区
- 恢复应用线程
适用场景:
- 客户端应用(如桌面程序)
- 内存较小的环境(如嵌入式设备)
- 单核 CPU 环境
启动参数:
-XX:+UseSerialGC # 新生代用 Serial,老年代用 Serial Old
优点:
- 简单高效
- 单线程开销低
- 没有线程交互开销
(2)Serial Old 收集器(老年代)
工作模式:
- 单线程收集
- STW 机制
- 采用标记-整理算法
适用场景:
- 与 Serial 收集器搭配使用
- 作为 CMS 收集器的后备收集器(当 CMS 出现 Concurrent Mode Failure 时)
- 用于 JDK 1.5 及之前版本的客户端应用
执行流程:
- 暂停所有应用线程
- 标记所有存活对象
- 将存活对象向堆的一端移动
- 清理边界外的内存
- 恢复应用线程
(3)ParNew 收集器(新生代)
工作模式:
- Serial 收集器的多线程版本
- STW 机制
- 采用复制算法
- 默认 GC 线程数等于 CPU 核心数
核心特点:
- 多线程并行回收
-XX:ParallelGCThreads=4 # 设置 GC 线程数 - 唯一能与 CMS 收集器配合的新生代收集器
- 在单核 CPU 上性能可能不如 Serial 收集器
适用场景:
- 服务端应用(如 Web 服务)
- 多核 CPU 环境
- 与 CMS 收集器搭配使用
启动参数:
-XX:+UseParNewGC # 启用 ParNew 收集器
性能考量:
- 在多核环境下能显著减少 STW 时间
- 线程调度有一定开销
- 适合中等规模的新生代
(4)CMS 收集器(老年代)
工作模式:
- 并发标记清除收集器
- 大部分阶段与用户线程并行
- 低延迟设计
- 采用标记-清除算法
执行流程:
-
初始标记(Initial Mark,STW):
- 标记 GC Roots 直接引用的对象
- 时间很短(通常 10-100ms)
-
并发标记(Concurrent Mark):
- 遍历整个老年代对象图
- 与用户线程并行执行
- 耗时较长(占整个 GC 时间的 80%)
-
重新标记(Remark,STW):
- 修正并发标记期间的变化
- 使用增量更新或原始快照技术
- 比初始标记长,但远短于并发标记
-
并发清除(Concurrent Sweep):
- 回收标记的垃圾对象
- 与用户线程并行执行
内存布局:
老年代内存使用 CMS 后的典型状态:
+--------+--------+--------+--------+
| 已用 | 空闲 | 碎片 | 已用 |
+--------+--------+--------+--------+
核心问题与解决方案:
-
Concurrent Mode Failure(CMF):
- 原因:并发清除期间老年代空间不足
- 表现:触发 Full GC,使用 Serial Old 收集器
- 解决方案:
-XX:CMSInitiatingOccupancyFraction=75 # 提前触发 CMS -XX:+UseCMSInitiatingOccupancyOnly
-
内存碎片化:
- 原因:标记-清除算法不整理内存
- 解决方案:
-XX:+UseCMSCompactAtFullCollection # Full GC 后整理 -XX:CMSFullGCsBeforeCompaction=4 # 每 4 次 Full GC 整理一次
-
浮动垃圾:
- 原因:并发标记期间新产生的垃圾
- 影响:需要预留足够空间(通常 20-30%)
适用场景:
- 对延迟敏感的服务端应用
- 老年代不特别大的情况(<8GB)
- 能容忍偶尔的 Full GC
启动参数:
-XX:+UseConcMarkSweepGC # 启用 CMS
-XX:+UseParNewGC # 自动启用 ParNew
-XX:CMSInitiatingOccupancyFraction=70 # 建议 70-80%
(5)G1 收集器(全堆)
工作模式:
- 分Region的内存布局
- 可预测停顿模型
- 并行与并发混合回收
- 标记-整理+复制混合算法
核心概念:
-
Region 划分:
- 堆被划分为多个大小相等的 Region(默认约 2048 个)
- 每个 Region 可以是 Eden、Survivor、Old 或 Humongous
- Humongous 区存储大对象(>50% Region 大小)
-
记忆集(Remembered Set):
- 每个 Region 有自己 RSet
- 记录其他 Region 对本 Region 的引用
- 避免全堆扫描
-
收集集合(Collection Set,CSet):
- 每次 GC 要回收的 Region 集合
- 根据垃圾比例优先选择
执行流程:
-
初始标记(Initial Mark,STW):
- 标记 GC Roots 直接关联的对象
- 与年轻代 GC 一起执行
-
并发标记(Concurrent Mark):
- 遍历对象图
- 与用户线程并行
-
最终标记(Final Mark,STW):
- 处理 SATB(Snapshot-At-The-Beginning)记录
- 完成标记
-
筛选回收(Live Data Counting and Evacuation,STW):
- 计算 Region 的回收价值
- 复制存活对象到空 Region
关键参数:
-XX:+UseG1GC # 启用 G1
-XX:MaxGCPauseMillis=200 # 目标停顿时间(毫秒)
-XX:G1HeapRegionSize=4m # Region 大小(1-32MB,2的幂)
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占比
适用场景:
- 堆内存较大(>6GB)
- 要求平衡吞吐量和延迟
- JDK 9+ 的默认收集器
优势:
- 并行与并发处理能力
- 分代收集但不分代内存
- 可预测的停顿模型
- 高效的整理算法
不足:
- 内存占用较高(RSet 等数据结构)
- 写屏障开销
- 小堆场景可能不如 CMS
4.4 GC 调优基础
4.4.1 关键性能指标
- 吞吐量:应用运行时间 / (应用运行时间 + GC 时间)
- 目标:通常 >95%
- 延迟:GC 导致的 STW 时间
- 目标:取决于应用需求(如 <200ms)
- 内存占用:完成功能所需的最小堆大小
4.4.2 通用调优步骤
-
监控分析:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log或使用可视化工具(如 GCViewer、Grafana)
-
设置基础参数:
-Xms4g -Xmx4g # 堆大小(生产环境建议相同) -XX:NewRatio=2 # 老年代/新生代比例 -
选择收集器:
- 吞吐量优先:Parallel Scavenge + Parallel Old
- 低延迟优先:ParNew + CMS 或 G1
- 超大堆(>8GB):G1 或 ZGC
-
优化分代大小:
-XX:SurvivorRatio=8 # Eden/Survivor 比例 -XX:MaxTenuringThreshold=15 # 晋升年龄 -
调整高级参数:
- CMS 相关参数
- G1 的 MaxGCPauseMillis
- 并行 GC 线程数
4.4.3 常见问题解决
-
频繁 Full GC:
- 检查内存泄漏
- 增大老年代或调整晋升阈值
- 优化对象分配
-
长 GC 停顿:
- 考虑切换到 G1 或 ZGC
- 减少每次回收的区域大小
- 优化引用结构
-
内存碎片化:
- 使用标记-整理算法
- 定期 Full GC 整理
- 考虑使用 G1
4.5 未来发展趋势
-
低延迟 GC:
- ZGC(JDK 11+):目标 <10ms 停顿
- Shenandoah(JDK 12+):与 ZGC 竞争
-
云原生适配:
- 容器内存感知
- 弹性堆大小调整
-
AI 辅助调优:
- 基于机器学习的自动参数调整
- 动态 GC 策略切换
-
异构内存支持:
- 持久内存(PMEM)支持
- 分级存储管理
选择合适的 GC 需要综合考虑应用特点、硬件环境和性能需求。随着 Java 发展,GC 技术也在不断进步,为不同场景提供更优解决方案。
五、GC 日志分析
GC 日志是排查 JVM 内存问题、优化 GC 性能的核心依据。通过分析GC日志,开发人员可以了解内存使用情况、垃圾回收效率以及潜在的性能瓶颈。JVM 默认不输出详细 GC 日志,需通过参数开启,不同收集器(如Serial、Parallel、CMS、G1等)的日志格式略有差异,但核心信息基本一致。
5.1 开启 GC 日志的核心参数
| 参数 | 作用 | 示例 | 说明 |
|---|---|---|---|
-XX:+PrintGCDetails | 输出详细 GC 日志(包含内存区域变化) | 必加参数 | 详细记录每次GC前后各内存区域(如Eden、Survivor、Old)的使用情况 |
-XX:+PrintGCTimeStamps | 输出 GC 发生的时间戳(相对于 JVM 启动时间) | 便于定位 GC 发生时机 | 格式为"123.456: [GC...",表示JVM启动123.456秒后发生GC |
-XX:+PrintGCDateStamps | 输出 GC 发生的具体日期时间(如 2025-09-10T15:30:00) | 便于关联业务日志 | 与系统日志时间对齐,方便问题排查 |
-Xloggc:./gc.log | 将 GC 日志输出到指定文件(避免控制台刷屏) | 生产环境必加,便于后续分析 | 建议路径为日志专用目录,如/var/log/gc.log |
-XX:+UseGCLogFileRotation | 开启 GC 日志轮转(避免单个日志文件过大) | 配合以下参数使用 | 当日志达到指定大小时自动创建新文件 |
-XX:NumberOfGCLogFiles=5 | 日志文件数量 | 保留 5 个日志文件 | 按时间顺序保留最近的5个日志 |
-XX:GCLogFileSize=100M | 单个日志文件大小 | 每个文件最大 100MB | 超过100MB时触发轮转 |
生产环境推荐配置示例:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/myapp/gc.log
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M
5.2 常见 GC 日志解析示例
5.2.1 ParNew + CMS 日志(新生代 Minor GC)
2025-09-10T15:30:00.123+0800: 10.456: [GC (Allocation Failure) [ParNew: 204800K->20480K(230400K), 0.0123450 secs] 204800K->40960K(983040K), 0.0124560 secs] [Times: user=0.04 sys=0.01, real=0.01 secs]
核心信息拆解:
-
时间信息:
2025-09-10T15:30:00.123+0800:GC 发生的具体时间(ISO8601格式,东八区)10.456:GC 发生时 JVM 已启动 10.456 秒
-
GC类型:
GC (Allocation Failure):表示这是一次 Minor GC,触发原因是 "新生代分配内存失败"(Eden区空间不足)
-
内存变化:
ParNew: 204800K->20480K(230400K):- ParNew:新生代收集器名称
- 204800K->20480K:GC前200MB → GC后20MB
- (230400K):新生代总容量225MB
204800K->40960K(983040K):- 堆内存从200MB → 40MB
- 堆总大小960MB
-
耗时统计:
0.0123450 secs:新生代GC实际耗时12毫秒(STW时间)[Times: user=0.04 sys=0.01, real=0.01 secs]:- user:所有GC线程消耗的CPU时间总和
- sys:系统调用耗时
- real:实际STW时间(多线程并行时,real < user)
5.2.2 G1 收集器日志(Mixed GC)
2025-09-10T15:35:00.678+0800: 40.987: [GC pause (G1 Evacuation Pause) (mixed), 0.0234560 secs]
[Parallel Time: 20.1 ms, GC Workers: 4]
[GC Worker Start (ms): 40987.1, 40987.2, 40987.3, 40987.4]
[Ext Root Scanning (ms): 5.2, 5.1, 5.0, 4.9]
[Update RS (ms): 2.1, 2.2, 2.3, 2.0]
[Processed Buffers: 10, 8, 9, 11]
[Scan RS (ms): 1.0, 1.1, 0.9, 1.2]
[Code Root Scanning (ms): 0.5, 0.4, 0.6, 0.5]
[Object Copy (ms): 10.3, 10.2, 10.4, 10.1]
[Termination (ms): 0.0, 0.0, 0.0, 0.0]
[Code Root Fixup: 0.2 ms]
[Code Root Purge: 0.1 ms]
[Clear CT: 0.3 ms]
[Other: 3.0 ms]
[Heap Before GC: 800.0M(1024.0M) -> Heap After GC: 400.0M(1024.0M)]
[Young Regions: 10->2 (total 20, 1024.0K each)]
[Old Regions: 50->30 (total 100, 1024.0K each)]
[Humongous Regions: 5->3 (total 10, 1024.0K each)]
核心信息拆解:
-
GC类型:
GC pause (G1 Evacuation Pause) (mixed):G1的混合回收,同时处理新生代和部分老年代Region
-
时间统计:
0.0234560 secs:总STW时间23毫秒Parallel Time: 20.1 ms, GC Workers: 4:4个GC线程并行工作耗时20.1毫秒
-
各阶段耗时:
Ext Root Scanning:扫描根对象(如栈、寄存器等)Update RS:更新Remembered SetObject Copy:复制存活对象的关键阶段Termination:GC线程结束工作
-
内存变化:
Heap Before GC: 800.0M(1024.0M) -> Heap After GC: 400.0M(1024.0M):堆内存减少400MB- Region统计:
Young Regions: 10->2:新生代Region从10个减少到2个Old Regions: 50->30:老年代Region回收20个Humongous Regions:大对象Region变化
-
其他信息:
Processed Buffers:每个GC线程处理的RSet缓冲区数量Code Root相关:处理代码缓存的时间
六、JVM 性能调优
核心目标与原则
JVM 调优的核心目标是:减少 GC 停顿时间、降低 GC 频率、避免 OOM 异常,最终提升应用的吞吐量和稳定性。调优需遵循"先监控,后调优"的原则,避免盲目修改参数。
在实际生产环境中,JVM调优是一个持续优化的过程,需要根据应用特性、业务量和硬件环境进行针对性调整。典型的应用场景包括:
- 高并发Web服务:关注低延迟和快速响应
- 大数据处理应用:关注高吞吐量
- 金融交易系统:关注极端低延迟
- 后台批处理作业:关注资源利用率
6.1 调优前的准备:关键监控指标
需通过工具监控以下指标,定位性能瓶颈:
内存指标
- 堆内存各区域使用情况:
- Eden区:新对象分配的主要区域
- Survivor区:存放Minor GC后存活的对象
- Old区:存放长期存活的对象
- 内存分配速率:每秒分配的内存量
- 晋升速率:对象从新生代晋升到老年代的速率
- 元空间使用情况:类元数据存储区域
GC 指标
- Minor GC频率:通常应控制在10-30秒一次
- Minor GC停顿时间:理想情况下应小于100ms
- Full GC频率:应控制在1小时以内
- Full GC停顿时间:单次停顿不应超过1秒
- GC吞吐量:GC时间占总运行时间的比例
线程指标
- 活跃线程数:反映当前并发处理能力
- 阻塞线程数:可能指示锁竞争或I/O瓶颈
- 死锁线程数:严重影响系统可用性
- 线程创建/销毁频率:过高可能影响性能
6.2 核心调优参数(按场景分类)
6.2.1 内存分配优化
堆内存设置
-
-Xms与-Xmx:- 建议设置为相同值(如
-Xms4g -Xmx4g) - 避免JVM频繁扩容/缩容带来的性能开销
- 典型配置:生产环境4GB-16GB,根据物理内存调整
- 建议设置为相同值(如
-
堆大小选择原则:
- 物理内存8GB:堆可设为4GB-6GB
- 物理内存16GB:堆可设为8GB-12GB
- 预留20%-30%内存给操作系统和其他进程
新生代优化
-
-Xmn:- 新生代大小,建议占堆的1/3-1/2
- 对于创建对象频繁的应用(如Web服务),可适当增大
- 示例:堆4GB,新生代可设为1.5GB
- 过小会导致频繁Minor GC,过大会减少老年代空间
-
-XX:SurvivorRatio:- Eden与Survivor区比例,默认8(Eden:S0:S1=8:1:1)
- 一般无需修改,除非有特殊需求
- 调整原则:
- 增大可容纳更多新对象
- 减小可延长对象在Survivor区的存活时间
老年代优化
- 大小由堆总大小和新生代大小决定
- 频繁Full GC可能原因:
- 大对象直接进入老年代
- 对象晋升过快
- 内存泄漏
- 调优策略:
- 检查对象生命周期
- 适当增大堆总大小
- 优化缓存策略
元空间优化(JDK 1.8+)
-XX:MetaspaceSize:- 元空间初始大小,默认约21MB
- 建议设置为256MB-512MB
-XX:MaxMetaspaceSize:- 元空间最大大小
- 防止无限制占用本地内存
- Metaspace OOM解决方案:
- 检查类加载器泄漏
- 增大元空间大小
- 优化动态类生成逻辑
6.2.2 GC 收集器选择与优化
收集器选择策略
| 应用类型 | 推荐收集器 | 适用场景 |
|---|---|---|
| 客户端应用 | Serial + Serial Old | 单线程、低开销 |
| 服务端应用(低延迟) | ParNew + CMS | 响应时间敏感型 |
| 服务端应用(大堆) | G1 | 平衡吞吐量与延迟 |
| 大数据处理 | Parallel Scavenge + Parallel Old | 高吞吐量优先 |
CMS收集器调优
-XX:+UseConcMarkSweepGC:启用CMS-XX:CMSInitiatingOccupancyFraction=75:- 老年代使用率达75%时触发CMS
- 避免Concurrent Mode Failure
-XX:+UseCMSCompactAtFullCollection:- Full GC后整理内存碎片
-XX:CMSFullGCsBeforeCompaction=3:- 每3次Full GC后整理一次
G1收集器调优
-XX:+UseG1GC:启用G1收集器-XX:MaxGCPauseMillis=100:- 设置目标停顿时间
- 金融场景建议50ms
-XX:G1HeapRegionSize=4m:- Region大小设置
- 堆16GB以下建议4MB-8MB
-XX:InitiatingHeapOccupancyPercent=45:- 堆使用率触发阈值
- 大堆可适当降低
6.2.3 其他关键调优参数
大对象处理
-XX:PretenureSizeThreshold=1048576:- 超过1MB的对象直接进入老年代
- 避免大对象在新生代频繁复制
- 需根据应用特点调整阈值
对象晋升年龄
-XX:MaxTenuringThreshold=15:- 对象晋升年龄阈值
- 可通过
-XX:+PrintTenuringDistribution监控 - 观察对象年龄分布调整
显式GC控制
-XX:+DisableExplicitGC:- 禁止System.gc()调用
- 生产环境建议开启
- 避免误触发Full GC
6.3 常见问题排查案例
案例1:频繁Full GC排查
现象:
- Full GC频率超过1次/分钟
- 单次停顿超过1秒
- 接口响应时间从50ms升至500ms+
排查步骤:
- 分析GC日志:
- 确认触发原因
- 检查内存不足模式
- 堆内存分析:
- 使用jmap生成堆dump
- MAT工具分析对象分布
- 元空间检查:
- jstat监控元空间使用
- 确认是否达到阈值
解决方案:
- 缓存优化:
- 引入Redis
- 设置合理过期时间
- 内存泄漏修复:
- 检查线程持有情况
- 清理无用引用
- 参数调整:
- 增大堆内存
- 调整元空间大小
案例2:G1停顿时间超标
现象:
- 设置MaxGCPauseMillis=100
- 实际STW超过200ms
排查步骤:
- GC日志分析:
- 确认耗时阶段
- 检查Region回收情况
- Region分布:
- 打印Region存活信息
- 分析对象分布
- 并发标记:
- 检查标记耗时
- 确认堆使用率
解决方案:
- 参数调整:
- 调整停顿时间目标
- 降低并发标记阈值
- Region优化:
- 调整Region大小
- 平衡回收粒度
6.4 JVM调优标准化流程
1. 确定目标
- 明确关键指标:
- GC频率
- 停顿时间
- 吞吐量
- 内存占用
2. 监控基准状态
- 收集基础数据:
- GC日志
- 堆内存统计
- 线程状态
- 使用分析工具:
- GCEasy
- VisualVM
- MAT
3. 定位瓶颈
- 高频GC:
- 检查新生代大小
- 优化对象分配
- 长停顿:
- 评估收集器选择
- 调整GC线程
- OOM:
- 分析堆dump
- 检查内存泄漏
4. 参数调整与验证
- 增量调整:
- 每次改1-2个参数
- 记录修改影响
- 效果验证:
- 运行足够时间
- 对比基准数据
5. 参数固化
- 更新启动脚本
- 文档记录:
- 调优过程
- 参数含义
- 效果对比
- 建立监控机制:
- 持续跟踪
- 异常告警
4455

被折叠的 条评论
为什么被折叠?



