JVM 内功修炼:运行时数据区、GC 演进与现代调优实践
- 约 23 分钟读完 - 全文 9069 字JVM 内功修炼:运行时数据区、GC 演进与现代调优实践
JVM 是 Java 程序员逃不掉的「操作系统」。懂它不是为了背八股,而是为了在 OOM、Full GC、CPU 飙升时,能一眼定位根因并给出调优方案。
一、高层次思想:JVM 在帮你解决什么问题?
JVM 的核心职责可以抽象成三件事:
- 一次编译,到处运行:通过字节码 + 解释/编译执行,屏蔽底层 OS 与硬件差异。
- 自动内存管理:让开发者专注于业务,而不是像 C/C++ 那样手动 malloc/free。
- 运行时优化:JIT 编译、逃逸分析、内联、锁消除等,把「解释执行」慢慢优化成「本地机器码执行」。
graph LR
A[.java 源码] -->|javac| B[字节码 .class]
B -->|类加载子系统| C[方法区/Metaspace]
C -->|解释器| D[逐行解释执行]
C -->|JIT 编译器| E[本地机器码]
D --> F[执行引擎]
E --> F
F --> G[操作系统]
避坑指南 #1:不要只把 JVM 当「黑盒」。线上 90% 的 Java 性能问题,根源都在堆外内存泄漏、GC 停顿过长、Metaspace 膨胀、线程栈溢出这四类。
二、运行时数据区:新版 JDK 的版图
不同虚拟机的运行时数据区实现略有差异,但都遵循《Java 虚拟机规范》。现代视角下,区域可划分为「线程私有」和「线程共享」两类:
graph TD
subgraph 线程私有
A[程序计数器 PC Register]
B[虚拟机栈 JVM Stack]
C[本地方法栈 Native Method Stack]
end
subgraph 线程共享
D[堆 Heap]
E[方法区 Method Area / Metaspace]
F[直接内存 Direct Memory]
end
A --> G[线程执行上下文]
B --> G
C --> G
D --> H[几乎所有对象实例]
E --> I[类元数据/常量/静态变量]
F --> J[NIO / 堆外内存]
2.1 程序计数器(Program Counter Register)
- 当前线程执行字节码的行号指示器。
- 分支、循环、跳转、异常处理、线程恢复都依赖它。
- 线程私有,生命周期与线程一致。
- 唯一一个在《Java 虚拟机规范》中没有规定 OOM 的区域。
2.2 Java 虚拟机栈(JVM Stack)
- 每个方法执行时创建一个栈帧(Stack Frame)。
- 栈帧里包含:局部变量表、操作数栈、动态链接、方法返回地址。
- 局部变量表中的基本数据类型和对象引用在编译期大小就确定。
graph TD
A[JVM Stack] --> B[栈帧 Frame 1]
A --> C[栈帧 Frame 2]
A --> D[栈帧 Frame 3]
B --> B1[局部变量表]
B --> B2[操作数栈]
B --> B3[动态链接]
B --> B4[方法返回地址]
避坑指南 #2:
-Xss设置线程栈大小。递归过深、方法调用链过长会抛出StackOverflowError;如果线程创建太多,总栈空间超过限制,会抛出OutOfMemoryError: unable to create new native thread。
2.3 本地方法栈(Native Method Stack)
- 与虚拟机栈作用相同,只是为 Native 方法服务。
- HotSpot 中把虚拟机栈和本地方法栈合二为一了。
2.4 Java 堆(Java Heap)
- JVM 管理的最大一块内存,被所有线程共享。
- 几乎所有对象实例和数组都在这里分配。
- 现代 GC 基本都采用分代收集,逻辑上分为新生代、老年代。
graph LR
A[Java Heap] --> B[Young Generation 新生代]
A --> C[Old Generation 老年代]
B --> D[Eden 区]
B --> E[Survivor From]
B --> F[Survivor To]
避坑指南 #3:不要把「堆」和「栈」对立记忆。对象在堆里,引用在栈里;但 HotSpot 的逃逸分析 + 标量替换会把未逃逸的小对象直接分配在栈上,减轻 GC 压力。
2.5 方法区(Method Area)与 Metaspace
- 存储类信息、常量、静态变量、JIT 编译后的代码缓存。
- JDK 7 之前:方法区在 PermGen(永久代)中,大小固定,
-XX:MaxPermSize。 - JDK 8 及以后:PermGen 被移除,方法区使用本地内存中的 Metaspace,默认只受限于系统内存,用
-XX:MaxMetaspaceSize控制。
| 版本 | 实现 | 配置参数 | 常见问题 |
|---|---|---|---|
| JDK 7- | PermGen | -XX:PermSize / -XX:MaxPermSize |
动态生成类过多导致 OutOfMemoryError: PermGen space |
| JDK 8+ | Metaspace | -XX:MetaspaceSize / -XX:MaxMetaspaceSize |
类加载器泄漏导致 Metaspace 持续增长 |
避坑指南 #4:Metaspace 默认无上限,服务运行久了可能把容器内存撑爆。线上建议显式设置
-XX:MaxMetaspaceSize,配合监控告警。
2.6 直接内存(Direct Memory)
- 不属于 JVM 堆,但会被 JVM 直接使用,常见于 NIO 的
DirectByteBuffer。 - 分配和回收成本较高,但读写 I/O 时避免了堆内外内存拷贝(Zero Copy)。
- 默认大小约等于
-Xmx,可用-XX:MaxDirectMemorySize控制。
避坑指南 #5:Netty、RocketMQ、Elasticsearch 等中间件大量使用堆外内存。出现
OutOfMemoryError: Direct buffer memory时,先检查是否有ByteBuf/DirectByteBuffer未释放,而不是盲目加堆内存。
三、垃圾回收:从算法到收集器演进
3.1 判断对象是否存活
- 引用计数法:Python 用,但 Java 不用,因为解决不了循环引用。
- 可达性分析(Reachability Analysis):从 GC Roots 出发,不可达则回收。
常见 GC Roots:
- 虚拟机栈中局部变量表引用的对象。
- 方法区中类静态属性、常量引用的对象。
- 本地方法栈中 JNI 引用的对象。
- 被同步锁持有的对象。
graph TD
A[GC Roots] --> B[栈帧局部变量]
A --> C[静态变量]
A --> D[常量]
A --> E[JNI 引用]
A --> F[锁持有对象]
B --> G[可达对象]
C --> G
D --> H[不可达对象<br/>等待回收]
3.2 四种基础 GC 算法
| 算法 | 思想 | 优点 | 缺点 |
|---|---|---|---|
| 标记-清除 | 标记无用对象,然后清除 | 简单,不需要移动对象 | 效率低,产生内存碎片 |
| 复制 | 内存分两块,活着的对象复制到另一块 | 无碎片,回收效率高 | 内存利用率只有一半 |
| 标记-整理 | 标记后把存活对象移到一端,清理边界外 | 无碎片,内存利用率高 | 移动对象开销大 |
| 分代收集 | 按对象存活周期分代,不同代用不同算法 | 兼顾吞吐与停顿 | 需要维护复杂的跨代引用 |
3.3 经典垃圾收集器
| 收集器 | 工作区域 | 算法 | 特点 | 适用场景 | 状态 |
|---|---|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程,简单高效 | 客户端模式 / 单核 | 可用 |
| ParNew | 新生代 | 复制 | Serial 的多线程版 | 配合 CMS | 随 CMS 移除 |
| Parallel Scavenge | 新生代 | 复制 | 吞吐量优先 | 后台计算 | 可用 |
| Serial Old | 老年代 | 标记-整理 | 单线程 | 客户端模式 | 可用 |
| Parallel Old | 老年代 | 标记-整理 | 多线程吞吐量优先 | 后台计算 | 可用 |
| CMS | 老年代 | 标记-清除 | 低停顿,并发收集 | 互联网低延迟服务 | JDK 14 已移除 |
| G1 | 整堆 | 标记-整理 + 复制 | 可控停顿, region 化 | 大堆、低延迟 | JDK 9-22 默认 |
| ZGC | 整堆 | 标记-复制/整理 | 亚毫秒停顿,TB 级堆 | 极低延迟 | JDK 23+ 默认(分代) |
| Shenandoah | 整堆 | 标记-复制/整理 | 低停顿,独立于分代 | 低延迟 | 可用,JDK 24 实验分代 |
graph LR
A[Serial] --> B[ParNew]
B --> C[Parallel Scavenge]
C --> D[G1]
D --> E[ZGC / Shenandoah]
style E fill:#f9f,stroke:#333
3.4 分代模型的工作流程
新生代默认占堆的 1/3,老年代占 2/3。新生代内部 Eden:From Survivor:To Survivor 默认约为 8:1:1。
sequenceDiagram
participant Eden
participant From as Survivor From
participant To as Survivor To
participant Old as Old Generation
Note over Eden,Old: Minor GC 触发
Eden->>To: 存活对象复制到 To
From->>To: 存活对象复制到 To
From->>Old: 年龄超过阈值的对象晋升
Note over Eden,From: 清空 Eden 和 From
To->>From: To 变 From,From 变 To
避坑指南 #6:不要把 Survivor 区看作「两个对称缓存区」。它们在一次 Minor GC 后会交换身份,始终有一个是空的,用于下一次复制目标。
3.5 G1 收集器:里程碑式的设计
G1(Garbage First)把堆划分为多个大小相等的 Region,每个 Region 动态扮演 Eden、Survivor 或 Old 角色。
graph TD
A[G1 Heap Regions] --> B[Eden Region]
A --> C[Survivor Region]
A --> D[Old Region]
A --> E[Humongous Region<br/>大对象]
A --> F[Free Region]
G1 的核心优势:
- 可预测的停顿时间模型:通过
-XX:MaxGCPauseMillis设定目标停顿。 - 整体基于标记-整理,局部基于复制:不会产生大面积内存碎片。
- 回收优先级按垃圾最多的 Region 来:Garbage First 名字来源。
避坑指南 #7:G1 的
-XX:MaxGCPauseMillis不是越小越好。设得太低(如 50ms)会导致回收 Region 太少、GC 频率飙升,反而降低吞吐。通常建议 100-200ms。
3.6 现代低延迟 GC:ZGC 与 Shenandoah
| 特性 | ZGC | Shenandoah |
|---|---|---|
| 设计目标 | 亚毫秒级停顿,支持 TB 级堆 | 低停顿,独立于分代 |
| 并发能力 | 标记、转移、重定位几乎都并发 | 并发标记、并发压缩 |
| 着色指针 | 使用 | 不使用 |
| 分代 | JDK 21 引入分代 ZGC,JDK 23 起为默认 | JDK 24 引入实验性分代 Shenandoah |
| 适用版本 | JDK 11+(生产推荐 JDK 17+) | OpenJDK 12+ |
graph LR
A[传统 GC<br/>Serial/Parallel/CMS/G1] -->|停顿目标 ms~百 ms 级| B[G1]
B -->|停顿目标 < 10ms| C[ZGC / Shenandoah]
C -->|JDK 21+| D[分代 ZGC]
D -->|JDK 23+| E[分代 ZGC 默认]
C -->|JDK 24+| F[分代 Shenandoah 实验]
高性能建议 #1:如果应用对延迟敏感(如金融交易、在线游戏、实时推荐),JDK 17 下优先考虑 G1;JDK 21+ 可直接启用 分代 ZGC(
-XX:+UseZGC -XX:+ZGenerational),在吞吐和延迟之间取得更好平衡;升级到 JDK 23+ 后分代 ZGC 已是默认,无需额外参数。
3.7 JDK 21/22/23/24 的 GC 演进大事记
| JDK 版本 | GC 相关变化 | 影响 |
|---|---|---|
| JDK 21 | JEP 439 正式发布分代 ZGC | 默认仍是 G1,但可显式启用 -XX:+ZGenerational 获得更低延迟 |
| JDK 22 | JEP 423 G1 Region Pinning | G1 在 JNI 临界区内无需禁用 GC,降低延迟毛刺 |
| JDK 23 | JEP 474 分代 ZGC 成为默认;非分代 ZGC 废弃 | 新默认 GC 从 G1 切换到分代 ZGC,亚毫秒级停顿更易获得 |
| JDK 23 | JDK-8322630 ZGC 延迟优化 | 减少 GC 根节点扫描抖动 |
| JDK 24 | JEP 490 移除非分代 ZGC | 简化 ZGC 维护,统一使用分代模式 |
| JDK 24 | JEP 404 分代 Shenandoah(实验性) | Shenandoah 也走向分代,未来低延迟 GC 竞争更激烈 |
| JDK 24 | JEP 475 G1 Late Barrier Expansion | 优化 G1 写屏障,提升编译和运行时性能 |
timeline
title GC 默认与里程碑
section JDK 8
默认 : Parallel Scavenge + Parallel Old
section JDK 9
默认 : G1
section JDK 11
ZGC 实验性
section JDK 15
ZGC 生产可用
section JDK 17
G1 默认,ZGC/Shenandoah 成熟
section JDK 21
分代 ZGC 正式发布(仍非默认)
section JDK 23
分代 ZGC 成为默认 GC
section JDK 24
非分代 ZGC 移除;分代 Shenandoah 实验
避坑指南 #7.5:网上有说法称 “JDK 21 默认 GC 是 ZGC”,这是错误的。JDK 21 默认仍是 G1;分代 ZGC 只是正式发布,需要显式开启。真正的默认切换发生在 JDK 23。
四、引用类型:不只是强引用
| 引用类型 | 回收时机 | 使用场景 |
|---|---|---|
| 强引用 | 永不回收,即使 OOM | 普通对象 |
| 软引用 SoftReference | 内存不足前回收 | 缓存,如图片缓存 |
| 弱引用 WeakReference | 下一次 GC 时回收 | 防止内存泄漏,如 WeakHashMap |
| 虚引用 PhantomReference | 随时可能被回收,必须配合 ReferenceQueue | 跟踪对象被回收的事件,做资源后置清理 |
graph LR
A[强引用] -->|最强| B[不会被回收]
C[软引用] -->|内存紧张| D[回收]
E[弱引用] -->|下次 GC| F[回收]
G[虚引用] -->|无法获取对象| H[仅回收通知]
避坑指南 #8:不要把软引用当「万能缓存」。在内存紧张时软引用会被批量回收,导致缓存命中率骤降。高并发缓存建议用 Caffeine 等带 TTL 和淘汰策略的专业缓存框架。
五、类加载机制:双亲委派与破坏
5.1 类加载器层次
graph TD
A[自定义类加载器] -->|委托| B[应用程序类加载器 AppClassLoader]
B -->|委托| C[平台类加载器 PlatformClassLoader]
C -->|委托| D[启动类加载器 BootstrapClassLoader]
注意:JDK 9 引入模块化系统后,原来的 Extension ClassLoader 被改名为 Platform ClassLoader,负责加载平台模块(如
java.sql、java.xml等)。
5.2 双亲委派模型
- 类加载器收到加载请求,先不自己加载。
- 向上委托给父加载器。
- 父加载器无法加载时,子加载器才尝试加载。
避坑指南 #9:Tomcat、SPI、OSGi 都破坏了双亲委派。如果你的项目出现
NoSuchMethodError/ClassCastException: cannot cast to the same class,很可能是同一个类被不同类加载器加载了两次。
六、JIT 编译与执行优化
Java 代码先被解释器执行,热点代码会被 JIT 编译成本地机器码。
6.1 HotSpot 的两层编译器
| 编译器 | 别称 | 特点 |
|---|---|---|
| C1 | Client Compiler | 编译快,优化简单,适合启动速度和 GUI |
| C2 | Server Compiler | 编译慢,激进优化,适合长期运行的服务端 |
JDK 7 引入分层编译(Tiered Compilation),JDK 8 默认开启:
0: 解释执行
1~3: C1 编译,带简单/复杂优化
4: C2 编译,激进优化6.2 常见 JIT 优化
| 优化 | 说明 |
|---|---|
| 方法内联 | 把短方法体直接嵌入调用处,减少栈帧开销 |
| 逃逸分析 | 判断对象是否逃出方法/线程作用域 |
| 标量替换 | 把对象拆成基本类型分配在栈上 |
| 锁消除 | 确认对象只被单线程访问时去掉同步锁 |
| 锁粗化 | 把相邻的同步块合并,减少加解锁次数 |
高性能建议 #2:不要过度纠结「每个对象都在堆上分配」。现代 JVM 的逃逸分析已经很强,写代码时优先保证对象作用域最小化、避免无意义的全局引用,让 JIT 有优化空间。
七、现代 JVM 前沿特性(JDK 21+)
除了 GC,JDK 21 及之后版本还在内存模型、执行模型和 Native 互操作上做了大量演进,这些也会间接影响 JVM 调优思路。
7.1 虚拟线程(Virtual Threads,JEP 444 / JDK 21)
- 虚拟线程是 JVM 管理的轻量级线程,由 JVM 调度到少量平台线程(Carrier Thread)上运行。
- 创建成本极低(KB 级栈),适合高并发 I/O 密集型应用。
- 对 JVM 数据区的影响:虚拟线程的栈帧仍存在于 Java 堆中(作为
Continuation对象),而不是传统的线程栈。
graph LR
A[平台线程 Platform Thread] -->|1:N 映射| B[虚拟线程 Virtual Thread]
B -->|挂载| C[Carrier Thread]
C -->|运行在| D[OS 线程]
避坑指南 #9.5:虚拟线程虽好,但不要和
synchronized块、本地方法、文件 I/O 混用导致 “Pinning”,会让载体线程被卡住,降低并发收益。优先使用ReentrantLock和异步 I/O。
7.2 Foreign Function & Memory API(JEP 454 / JDK 22)
- 正式替代
sun.misc.Unsafe进行堆外内存操作。 - 通过
Arena管理堆外内存生命周期,更安全、更可移植。 - 对直接内存调优的影响:未来
DirectByteBuffer的底层实现会逐步迁移到 FFM API。
7.3 Project Lilliput:压缩对象头
- 目标是把 HotSpot 的对象头从 96/128 bit 压缩到 64 bit。
- 可以显著降低内存占用,提升缓存命中率。
- JDK 24 起部分功能已集成(如 64-bit object headers)。
7.4 Project Valhalla:值类型(Value Classes / Primitive Classes)
- 引入 “值对象”,避免对象头和引用开销。
- 未来
int[]和Integer[]之间的性能鸿沟会缩小。 - 目前处于预览阶段,持续关注即可。
7.5 Project Leyden:Java 静态镜像
- 目标是通过 “静态图像” 加速 Java 启动时间。
- 类似 GraalVM Native Image,但保持完整 Java 语义和 JVM 运行时能力。
- 对 Serverless、CLI 工具、云原生场景意义重大。
八、避坑指南:线上常见问题
8.1 慎用 System.gc()
- 只是「建议」JVM 进行 Full GC,不保证执行。
- 很多框架(如某些老版本 JDBC)会调用它,导致不可控停顿。
- 线上建议关闭:
-XX:+DisableExplicitGC。
8.2 finalize() 已过时
- JDK 9 起标记为
@Deprecated。 - 对象回收前不保证一定执行
finalize(),且执行顺序不确定。 - 资源清理用
try-with-resources或Cleaner。
8.3 大对象与 Humongous
- G1 中超过 Region 大小一半的对象叫 Humongous Object,直接进老年代,增加 GC 负担。
- 避免一次性创建超大数组或字符串。
8.4 动态代理与 CGLIB 的 Metaspace 泄漏
- Spring AOP、MyBatis、Dubbo 大量生成代理类。
- 如果 ClassLoader 没释放,代理类元数据会持续占用 Metaspace。
- 常见于热部署、OSGi、动态脚本场景。
8.5 大堆 + Full GC 的雪崩
- 堆内存越大,G1 的 Mixed GC、CMS 的 Full GC 停顿越长。
- 超过 32GB 时,指针压缩会失效,对象头变大,反而浪费内存。
- 建议堆大小控制在 16GB-32GB 之间,再大的堆考虑横向拆分服务或换 ZGC。
九、高性能调优建议
9.1 选择 GC 与 JDK 版本
| 场景 | 推荐配置 |
|---|---|
| 通用 Web 服务,JDK 17-22 | -XX:+UseG1GC -XX:MaxGCPauseMillis=200 |
| 通用 Web 服务,JDK 23+ | 默认已是分代 ZGC,一般无需改动 |
| 延迟敏感,JDK 21-22 | -XX:+UseZGC -XX:+ZGenerational |
| 延迟敏感,JDK 23+ | 默认分代 ZGC,无需显式开启 |
| 批处理/大数据,吞吐优先 | -XX:+UseParallelGC 或 G1 |
| 容器环境 | 显式设置 -Xmx、-Xms、-XX:MaxMetaspaceSize,并开启 -XX:+UseContainerSupport |
9.2 参数模板
JDK 17-22 通用 Web 服务(G1)
java \
-Xms4g -Xmx4g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-jar app.jarJDK 23+ 通用 Web 服务(默认分代 ZGC)
java \
-Xms4g -Xmx4g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar app.jar说明:JDK 23 起默认就是分代 ZGC,不需要
-XX:+UseZGC。如果仍想显式启用,可保留;如果想用回 G1,使用-XX:+UseG1GC。
9.3 监控与诊断三板斧
| 工具 | 用途 |
|---|---|
jstat -gcutil <pid> 1000 |
实时查看 GC 频率与内存占比 |
jmap -dump:format=b,file=... <pid> |
生成堆快照,MAT 分析内存泄漏 |
jstack <pid> |
打印线程栈,排查死锁与 CPU 热点 |
GC 日志 / -Xlog:gc* |
分析 GC 原因与停顿时间 |
async-profiler |
低开销 CPU/内存/锁分析 |
高性能建议 #3:调优前一定先定目标——是降低停顿?提高吞吐?还是减少内存?没有目标的调优就是瞎调。先用 GC 日志和监控找到瓶颈,再改参数,而不是反向操作。
十、JVM 问题排查实战:发现 → 定位 → 修复
线上 JVM 问题通常表现为:服务卡顿、响应 P99 飙高、CPU 打满、内存持续增长、频繁 OOM。解决它们的核心思路是:先量化,再猜测。
10.1 问题发现的四层入口
graph TD
A[业务层] -->|接口超时/P99 飙升| B[应用层]
B -->|Full GC 频繁/GC 时间长| C[JVM 层]
C -->|堆/栈/Metaspace 异常| D[系统层]
D -->|容器 OOMKilled/CPU 限流| E[基础设施层]
| 入口 | 典型现象 | 常见根因 |
|---|---|---|
| 业务监控 | 接口 RT 上涨、错误率增加 | GC 停顿、线程阻塞、锁竞争 |
| APM / Trace | 某段链路偶发耗时几百 ms | Full GC、Metaspace 扩容、类加载 |
| 容器监控 | Pod 内存触顶被 OOMKilled | 堆外内存泄漏、堆太大、Metaspace 无上限 |
| GC 日志 | GC 频率异常、停顿时间超标 | 收集器选择不当、堆大小不合理、大对象过多 |
| JVM 告警 | Heap 使用率 > 85%、FGC 次数突增 | 内存泄漏、流量突增、缓存失控 |
10.2 排查工具箱
10.2.1 命令行工具(适合快速止血)
| 命令 | 场景 | 输出重点 |
|---|---|---|
jps -lvm |
找到 Java 进程 PID | 主类名、启动参数 |
jstat -gcutil <pid> 1000 |
实时监控 GC 与内存 | S0/S1/E/O/M 各区使用率、YGC/FGC 次数 |
jmap -heap <pid> |
查看堆配置与概览 | 新生代/老年代大小、收集器 |
jmap -histo <pid> |
查看对象直方图 | 哪个类实例数/占用最多 |
jmap -dump:format=b,file=... <pid> |
生成堆 dump | 配合 MAT / JProfiler 分析 |
jstack <pid> |
打印线程栈 | 死锁、CPU 热点线程、Blocked 线程 |
jcmd <pid> VM.native_memory summary |
查看 Native Memory Tracking | 堆外内存、Metaspace、Symbol 区占用 |
10.2.2 可视化 / 专业工具
| 工具 | 适用场景 | 特点 |
|---|---|---|
| MAT (Eclipse Memory Analyzer) | 分析堆 dump | 大对象、内存泄漏、支配树、Leak Suspects |
| JProfiler / YourKit | 全链路 JVM 分析 | CPU、内存、线程、GC 一体化 |
| async-profiler | 生产环境火焰图 | 低开销,适合线上持续采样 |
| GCViewer / GCEasy | 解析 GC 日志 | 可视化 GC 事件、停顿分布 |
| Prometheus + Grafana | 长期监控与告警 | 自定义 JVM 指标大盘 |
| Arthas | 线上实时诊断 | 无需重启,支持 trace、watch、thread、heapdump |
10.3 典型问题排查流程
场景 1:CPU 飙升
# 1. 找到 CPU 占用最高的线程
ps -mp <pid> -o THREAD,tid,time | sort -rn
# 2. 把 tid 转成十六进制
printf "%x\n" <tid>
# 3. 用 jstack 查看对应线程栈
jstack <pid> | grep -A 30 <hex_tid>常见根因:
- 死循环或复杂正则
- Young GC 过于频繁
- JIT 编译器 C2 线程忙碌(启动期正常)
- 大量
ThreadLocal未清理
场景 2:内存持续增长 / OOM
# 1. 确认哪个区域增长
jstat -gcutil <pid> 1000
# 2. 开启 Native Memory Tracking
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory summary.diff
# 3. 触发堆 dump(若还能响应)
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>常见根因:
- 堆:缓存无淘汰、大对象、集合只增不减
- Metaspace:动态代理/CGLIB 类加载器泄漏
- 堆外:Netty
ByteBuf未释放、NIO DirectBuffer 堆积
场景 3:接口偶发卡顿 / P99 高
- 打开 GC 日志,看是否有 长停顿。
- 用
jstack抓取卡顿时刻线程栈,看是否有大量BLOCKED。 - 用
async-profiler采集 CPU 火焰图,定位热点方法。 - 检查是否有
System.gc()、Metaspace 扩容、类加载抖动。
10.4 JVM 监控告警建议
建议把以下指标接入 Prometheus + Grafana,并设置合理阈值:
| 指标 | 告警阈值建议 | 说明 |
|---|---|---|
jvm_memory_used_bytes / jvm_memory_max_bytes |
> 85% | 堆/非堆内存使用率 |
jvm_gc_pause_seconds_sum |
P99 > 200ms | GC 停顿时间 |
jvm_gc_memory_allocated_bytes_total 增长率 |
突增 2 倍以上 | 对象分配速率异常 |
jvm_classes_loaded_total |
持续增长 | 类泄漏嫌疑 |
process_cpu_usage |
> 80% 持续 5min | CPU 压力 |
jvm_threads_live |
突增或接近上限 | 线程泄漏 |
容器维度 container_memory_working_set_bytes |
> 90% limit | 容器 OOMKilled 预警 |
graph LR
A[应用埋点] -->|Micrometer| B[Prometheus]
C[GC 日志] -->|Loki / Filebeat| D[日志平台]
B --> E[Grafana 大盘]
D --> E
E -->|阈值触发| F[AlertManager]
F -->|通知| G[钉钉/企业微信/Slack]
十一、不同场景的 JVM 配置策略
11.1 配置前的通用原则
- 先压测,再调优:没有真实流量数据,调参都是拍脑袋。
- Xms = Xmx:避免运行时堆扩容带来的 GC 抖动。
- Metaspace 必须设上限:容器环境下尤其重要。
- GC 日志一定要开:
-Xlog:gc*(JDK 9+)成本很低,信息无价。 - 容器环境开启 Container Support:
-XX:+UseContainerSupport(JDK 8u191+/10+ 默认开启)。 - 不要堆内存无限大:超过 32GB 指针压缩失效,性价比骤降。
11.2 场景一:高并发 Web / API 服务
特征:QPS 高、对象生命周期短、响应延迟敏感。
目标:降低 GC 停顿、控制 P99。
推荐配置(JDK 17-22,G1):
java \
-Xms4g -Xmx4g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:InitiatingHeapOccupancyPercent=35 \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar app.jar推荐配置(JDK 23+,默认分代 ZGC):
java \
-Xms4g -Xmx4g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar app.jar调优要点:
- 新生代尽量大,让短生命周期对象在 Young GC 回收。
-XX:MaxGCPauseMillis根据业务容忍度设置,100-200ms 常见。- 监控
jvm_gc_pause_seconds_sum,若频繁触发 Mixed GC,考虑增大-XX:InitiatingHeapOccupancyPercent。
11.3 场景二:高流量 / 削峰填谷(消息队列消费者)
特征:突发流量、消息处理批量化、吞吐优先、允许一定延迟。
目标:提高吞吐,避免 OOM 和 Full GC。
推荐配置:
java \
-Xms8g -Xmx8g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=300 \
-XX:G1HeapRegionSize=16m \
-XX:InitiatingHeapOccupancyPercent=40 \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar consumer.jar调优要点:
- 堆可以比 Web 服务稍大,因为要缓冲突发消息。
- 批量消费时注意控制单次拉取消息数,避免一次创建过多对象。
- 如果业务允许更高延迟,可考虑
-XX:+UseParallelGC追求极致吞吐。
11.4 场景三:低延迟金融 / 交易 / 实时推荐
特征:P99 要求 < 10ms,任何 GC 停顿都可能导致业务损失。
目标:亚毫秒级 GC 停顿。
推荐配置(JDK 21+):
java \
-Xms16g -Xmx16g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar trading.jarJDK 23+ 可简化为(默认就是分代 ZGC):
java \
-Xms16g -Xmx16g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar trading.jar调优要点:
- 堆 > 16GB 时 ZGC 优势更明显。
- 避免大对象(Humongous),ZGC 对大对象分配路径不如小对象高效。
- 监控
jvm_gc_pause_seconds_max,正常应在 1ms 以内。
11.5 场景四:大数据 / 批处理 / ETL
特征:长时间运行、对象存活久、吞吐优先、延迟不敏感。
目标:最大化吞吐,减少总 GC 时间。
推荐配置:
java \
-Xms16g -Xmx16g \
-XX:MetaspaceSize=512m \
-XX:MaxMetaspaceSize=1g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=8 \
-XX:+UseLargePages \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar etl.jar调优要点:
- Parallel GC 在纯吞吐场景下通常优于 G1。
- 大堆 + 大页(LargePages)减少 TLB miss。
- 如果批处理任务会频繁 Full GC,考虑分阶段释放引用或使用堆外缓存。
11.6 场景五:容器 / K8s 微服务
特征:资源受限、弹性伸缩、多实例共享节点。
目标:稳定、不 OOMKilled、启动快。
推荐配置:
java \
-XX:+UseContainerSupport \
-XX:InitialRAMPercentage=70.0 \
-XX:MaxRAMPercentage=70.0 \
-XX:MetaspaceSize=128m \
-XX:MaxMetaspaceSize=256m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=50m \
-jar service.jar调优要点:
- 用
-XX:MaxRAMPercentage替代固定-Xmx,适配不同 Limit。 - 容器 memory limit 要预留 10%-20% 给堆外内存、Metaspace、线程栈、JVM 自身。
- 不要把
-Xmx设成容器 limit 的 100%,否则容易被 OOMKilled。 - JDK 23+ 在容器里可直接用默认分代 ZGC。
11.7 场景六:Spring Boot / 微服务网关
特征:启动时大量类加载、反射多、动态代理多。
目标:启动快 + 运行稳。
推荐配置:
java \
-Xms2g -Xmx2g \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=150 \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
-Dspring.backgroundpreinitializer.ignore=true \
-jar gateway.jar调优要点:
- Spring Boot 启动时 Metaspace 会快速上涨,初始值不要设太小。
- 如果启动慢,可用
-XX:+TieredCompilation配合-XX:TieredStopAtLevel=1加速启动(牺牲峰值性能)。 - 网关避免在请求路径创建大对象,优先复用对象和缓冲区。
十二、一句话总结
- 内存:堆内分代、堆外 Direct、Metaspace 存类元数据;虚拟线程栈帧在堆中。
- GC:Serial → Parallel → CMS → G1 → 分代 ZGC;JDK 21 分代 ZGC 正式发布,JDK 23 起成为默认 GC。
- 引用:强 > 软 > 弱 > 虚,缓存别滥用软引用。
- 性能:JDK 17-22 优先 G1;JDK 23+ 默认分代 ZGC;延迟极致用 ZGC,吞吐优先用 Parallel。
- 排查:监控先行 → 日志定位 → 工具分析 → 压测验证 → 持续观察。
- 场景:Web 服务控延迟、消息队列保吞吐、金融交易用 ZGC、批处理用 Parallel、容器环境留余量。
JVM 调优没有银弹,但有方法论:理解数据区 → 选对收集器 → 设定合理目标 → 用日志验证 → 持续监控。把这套流程跑熟,面试八股和线上救火就都不慌了。
参考
- 《深入理解 Java 虚拟机》(第三版)周志明
- OpenJDK HotSpot Virtual Machine Garbage Collection Tuning Guide
- JEP 333: ZGC
- JEP 439: Generational ZGC
- JEP 474: ZGC Generational Mode by Default
- JEP 490: Remove the Non-Generational ZGC Mode
- JEP 404: Generational Shenandoah (Experimental)
- JEP 444: Virtual Threads
- JEP 454: Foreign Function & Memory API
- Project Lilliput