本文是一份作者总结系统化的 JVM 调优参考文档,涵盖内存模型、GC 回收器选型、参数配置、监控工具、问题诊断与调优决策全流程。适用于生产环境故障排查与性能优化场景。
一、JVM 内存模型
1.1 运行时数据区域
#mermaid-svg-JWdO0ns36HSGBIvs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JWdO0ns36HSGBIvs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JWdO0ns36HSGBIvs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JWdO0ns36HSGBIvs .error-icon{fill:#552222;}#mermaid-svg-JWdO0ns36HSGBIvs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JWdO0ns36HSGBIvs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JWdO0ns36HSGBIvs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JWdO0ns36HSGBIvs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JWdO0ns36HSGBIvs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JWdO0ns36HSGBIvs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JWdO0ns36HSGBIvs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JWdO0ns36HSGBIvs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JWdO0ns36HSGBIvs .marker.cross{stroke:#333333;}#mermaid-svg-JWdO0ns36HSGBIvs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JWdO0ns36HSGBIvs p{margin:0;}#mermaid-svg-JWdO0ns36HSGBIvs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JWdO0ns36HSGBIvs .cluster-label text{fill:#333;}#mermaid-svg-JWdO0ns36HSGBIvs .cluster-label span{color:#333;}#mermaid-svg-JWdO0ns36HSGBIvs .cluster-label span p{background-color:transparent;}#mermaid-svg-JWdO0ns36HSGBIvs .label text,#mermaid-svg-JWdO0ns36HSGBIvs span{fill:#333;color:#333;}#mermaid-svg-JWdO0ns36HSGBIvs .node rect,#mermaid-svg-JWdO0ns36HSGBIvs .node circle,#mermaid-svg-JWdO0ns36HSGBIvs .node ellipse,#mermaid-svg-JWdO0ns36HSGBIvs .node polygon,#mermaid-svg-JWdO0ns36HSGBIvs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JWdO0ns36HSGBIvs .rough-node .label text,#mermaid-svg-JWdO0ns36HSGBIvs .node .label text,#mermaid-svg-JWdO0ns36HSGBIvs .image-shape .label,#mermaid-svg-JWdO0ns36HSGBIvs .icon-shape .label{text-anchor:middle;}#mermaid-svg-JWdO0ns36HSGBIvs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JWdO0ns36HSGBIvs .rough-node .label,#mermaid-svg-JWdO0ns36HSGBIvs .node .label,#mermaid-svg-JWdO0ns36HSGBIvs .image-shape .label,#mermaid-svg-JWdO0ns36HSGBIvs .icon-shape .label{text-align:center;}#mermaid-svg-JWdO0ns36HSGBIvs .node.clickable{cursor:pointer;}#mermaid-svg-JWdO0ns36HSGBIvs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JWdO0ns36HSGBIvs .arrowheadPath{fill:#333333;}#mermaid-svg-JWdO0ns36HSGBIvs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JWdO0ns36HSGBIvs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JWdO0ns36HSGBIvs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JWdO0ns36HSGBIvs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JWdO0ns36HSGBIvs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JWdO0ns36HSGBIvs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JWdO0ns36HSGBIvs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JWdO0ns36HSGBIvs .cluster text{fill:#333;}#mermaid-svg-JWdO0ns36HSGBIvs .cluster span{color:#333;}#mermaid-svg-JWdO0ns36HSGBIvs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JWdO0ns36HSGBIvs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JWdO0ns36HSGBIvs rect.text{fill:none;stroke-width:0;}#mermaid-svg-JWdO0ns36HSGBIvs .icon-shape,#mermaid-svg-JWdO0ns36HSGBIvs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JWdO0ns36HSGBIvs .icon-shape p,#mermaid-svg-JWdO0ns36HSGBIvs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JWdO0ns36HSGBIvs .icon-shape .label rect,#mermaid-svg-JWdO0ns36HSGBIvs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JWdO0ns36HSGBIvs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JWdO0ns36HSGBIvs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JWdO0ns36HSGBIvs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-JWdO0ns36HSGBIvs .privateStyle>*{fill:#3498DB!important;color:#fff!important;stroke:#1A5276!important;stroke-width:2px!important;font-weight:bold!important;}#mermaid-svg-JWdO0ns36HSGBIvs .privateStyle span{fill:#3498DB!important;color:#fff!important;stroke:#1A5276!important;stroke-width:2px!important;font-weight:bold!important;}#mermaid-svg-JWdO0ns36HSGBIvs .privateStyle tspan{fill:#fff!important;}#mermaid-svg-JWdO0ns36HSGBIvs .sharedStyle>*{fill:#E67E22!important;color:#fff!important;stroke:#7E5109!important;stroke-width:2px!important;font-weight:bold!important;}#mermaid-svg-JWdO0ns36HSGBIvs .sharedStyle span{fill:#E67E22!important;color:#fff!important;stroke:#7E5109!important;stroke-width:2px!important;font-weight:bold!important;}#mermaid-svg-JWdO0ns36HSGBIvs .sharedStyle tspan{fill:#fff!important;}#mermaid-svg-JWdO0ns36HSGBIvs .defaultStyle>*{fill:#ECF0F1!important;color:#2C3E50!important;stroke:#BDC3C7!important;}#mermaid-svg-JWdO0ns36HSGBIvs .defaultStyle span{fill:#ECF0F1!important;color:#2C3E50!important;stroke:#BDC3C7!important;}#mermaid-svg-JWdO0ns36HSGBIvs .defaultStyle tspan{fill:#2C3E50!important;} Java 8 HotSpot JVM 运行时数据区域
🌐 堆内存 (Heap) - 线程共享
新生代 (Young Generation)
YGC 复制 (年龄+1)
反复交换
晋升 (年龄≥阈值)
Eden (80%)
Survivor0 (10%)
Survivor1 (10%)
老年代 (Old Generation)
非堆内存 (Non-Heap)
🌐 线程共享 (Thread-Shared)
元空间 (Metaspace)
(方法区的 HotSpot 实现)
内含: 运行时常量池
(每个类/接口的常量池)
直接内存 (Direct Memory)
(NIO 堆外内存)
CodeCache
(JIT 编译代码缓存)
🧵 线程私有 (Thread-Private)
pc 寄存器
(记录字节码指令地址)
Java 虚拟机栈
(-Xss 控制大小)
本地方法栈
(支持 native 方法)
1.2 各区域说明
以下是完整的分类表格:
🧵 线程私有(Thread-Private)
| 区域名称 | 所属范畴 | 说明 |
|---|---|---|
| pc 寄存器 | JVM 规范 | 记录当前线程执行的字节码指令地址,线程切换后用于恢复执行位置 |
| Java 虚拟机栈(HotSpot 中即"线程栈") | JVM 规范 | 存储每个方法的栈帧(局部变量、操作数栈、方法返回地址等),由 -Xss 控制大小 |
| 本地方法栈 | JVM 规范 | 为 native 方法服务,在 HotSpot 中与 Java 虚拟机栈合二为一 |
🌐 线程共享(Thread-Shared)
| 区域名称 | 所属范畴 | 说明 |
|---|---|---|
| 堆(Heap) | JVM 规范 | 存放所有类实例和数组,GC 管理的主要区域(HotSpot 进一步分代为 Eden/S0/S1/老年代) |
| 方法区(Method Area) | JVM 规范 | 存储类结构信息(字段、方法数据、构造器代码等) |
| 运行时常量池 | JVM 规范 | 每个类/接口的常量池,分配在方法区内 |
| 元空间(Metaspace) | HotSpot 特有(方法区的实现) | 使用本地内存存放类的元数据,替代永久代 |
| 直接内存(Direct Memory) | HotSpot 特有 | 堆外内存,供 NIO 使用,通过 -XX:MaxDirectMemorySize 限制 |
| CodeCache | HotSpot 特有 | 存储 JIT 编译器生成的本地机器码,供所有线程调用 |
1.3 对象分配与流转规则
对象分配位置:
| 分配位置 | 条件 |
|---|---|
| Eden 区 | 绝大多数对象在此分配 |
| 老年代 | 大对象(超过 -XX:PretenureSizeThreshold) |
| TLAB(Thread Local Allocation Buffer) | 线程本地分配缓冲区,位于 Eden 区内部 |
晋升到老年代的条件:
| 条件 | 说明 | 控制参数 |
|---|---|---|
| 年龄达到阈值 | 每经历一次 YGC 且存活,年龄 +1 | -XX:MaxTenuringThreshold(默认 15) |
| 动态年龄判定 | Survivor 中某年龄及以上对象总大小超过 Survivor 一半时,该年龄及以上对象提前晋升 | JVM 自动计算 |
| 提前晋升 | YGC 时存活对象 > Survivor 可用空间,全部直接晋升 | -XX:SurvivorRatio 间接控制 |
| 大对象直接分配 | 超过阈值的大小直接在老年代分配 | -XX:PretenureSizeThreshold |
二、GC 回收器类型与选型指南
2.1 回收器类型总览
Java 8 中可用的四种垃圾回收器,按设计目标分类:
| 回收器 | 设计目标 | 线程模型 | 核心特点 | 适用场景 |
|---|---|---|---|---|
| Serial | 单线程效率 | 单线程,STW | 实现简单,无线程开销 | 客户端应用,小内存(< 1GB) |
| Parallel GC(默认) | 高吞吐量 | 多线程,STW | 吞吐量最高,Full GC 扫描全堆 | 批处理,后台任务,计算密集型 |
| CMS | 低延迟 | 多线程,并发 | 暂停时间短,存在碎片问题 | Web 应用,交互式系统 |
| G1 | 可预测停顿 | 多线程,并发+并行 | 分区回收,停顿可控,内存压缩 | 大堆内存(> 4GB) |
设计目标的权衡关系:
吞吐量和低延迟是相互制约的。追求高吞吐量意味着更长的 STW 暂停;追求低延迟意味着更频繁的并发回收,占用 CPU 资源,牺牲吞吐量。
2.2 各回收器详解
Serial GC(串行回收器)
| 属性 | 说明 |
|---|---|
| 启用参数 | -XX:+UseSerialGC |
| 年轻代回收 | Serial(单线程,STW,复制) |
| 老年代回收 | Serial Old(单线程,STW,标记-整理) |
| 设计目标 | 单线程效率 |
| 最佳堆大小 | < 1GB |
回收时只使用一个线程,暂停所有应用线程。现代服务器多为多核 CPU,无法利用多核优势。适用于单核 CPU 或内存小于 1GB 的客户端应用。
Parallel GC(并行回收器)------ Java 8 默认
| 属性 | 说明 |
|---|---|
| 启用参数 | -XX:+UseParallelGC(自动启用 -XX:+UseParallelOldGC) |
| 年轻代回收 | Parallel Scavenge(多线程,STW,复制) |
| 老年代回收 | Parallel Old(多线程,STW,标记-整理) |
| 设计目标 | 高吞吐量优先 |
| 最佳堆大小 | 4GB - 8GB |
年轻代和老年代回收均使用多线程并行执行,但会暂停所有应用线程。JVM 会根据运行情况自适应调整各区域大小(-XX:+UseAdaptiveSizePolicy),以达到设定的吞吐量目标。
优点:吞吐量最高(GC 时间占比最小);回收后内存压缩,无碎片问题。缺点:Full GC 需扫描整个堆,堆越大停顿越长。
适用于批处理任务、后台作业、对响应时间不敏感的计算密集型应用。不适用于 Web 应用或交互式系统。
CMS(Concurrent Mark Sweep)回收器
| 属性 | 说明 |
|---|---|
| 启用参数 | -XX:+UseConcMarkSweepGC(需配合 -XX:+UseParNewGC) |
| 年轻代回收 | ParNew(多线程,STW,复制) |
| 老年代回收 | CMS(多线程,并发,标记-清除) |
| 设计目标 | 低延迟优先 |
| 最佳堆大小 | 4GB - 8GB |
老年代回收的大部分阶段(并发标记、并发清理)与应用线程并发执行,不暂停应用。只在初始标记和最终标记阶段短暂 STW。
回收阶段:初始标记(STW,短暂)→ 并发标记(应用运行)→ 最终标记(STW,短暂)→ 并发清理(应用运行)
优点:老年代回收时应用不暂停,GC 停顿时间短(毫秒级)。缺点:存在内存碎片(标记-清除算法不压缩);并发回收占用 CPU 资源;可能发生并发模式失败(回收速度 < 对象分配速度,退化为 Serial Full GC)。
适用于 Web 应用、微服务、对响应时间要求高的交互式系统。不适用于大堆(> 16GB)或批处理任务。
G1(Garbage-First)回收器
| 属性 | 说明 |
|---|---|
| 启用参数 | -XX:+UseG1GC |
| 堆结构 | 分区(Region),不区分固定年轻代/老年代 |
| 设计目标 | 可预测停顿 |
| 最佳堆大小 | > 4GB |
将整个堆划分为若干个大小相等的 Region(默认约 2048 个)。每个 Region 可以扮演 Eden、Survivor 或老年代的角色。G1 优先回收垃圾最多的 Region(Garbage-First 名称的来源)。
与 Parallel GC/CMS 的本质区别:G1 没有固定的年轻代/老年代物理边界,回收粒度是部分 Region 而非整个代;停顿时间可通过 -XX:MaxGCPauseMillis 控制;分区回收自带内存压缩,无碎片问题。
优点:停顿时间可预测;分区回收不扫描全堆;自带内存压缩无碎片;适应性最强。缺点:Java 8 早期版本(u40 之前)不稳定;相比 Parallel GC,吞吐量略低。
适用于堆大小 > 4GB、对停顿时间有明确要求的应用,是 Java 8+ 的通用推荐(尤其是 Java 8u40 之后)。
2.3 选型决策矩阵
基于堆大小的选型建议:
| 堆大小 | 推荐回收器 | 理由 |
|---|---|---|
| < 1GB | Serial | 单线程效率最高,无多线程开销 |
| 1GB - 4GB | Parallel GC 或 CMS | 并行回收吞吐量高;CMS 可降低延迟 |
| 4GB - 8GB | CMS 或 G1 | CMS 成熟稳定;G1 在 Java 8u40 后成熟 |
| > 8GB | G1 | 分区回收避免全堆扫描,停顿可控 |
基于业务场景的选型建议:
| 业务场景 | 推荐回收器 | 理由 |
|---|---|---|
| 批处理/离线计算 | Parallel GC | 吞吐量优先 |
| Web 应用(4-8GB) | CMS | 低延迟优先 |
| Web 应用(> 8GB) | G1 | 可控停顿,无碎片 |
| 微服务(容器化) | G1 | 适应性最强 |
| 通用推荐(Java 8+) | G1 | 综合最优 |
选型决策树:
text
开始
│
├── 堆内存 < 1GB?
│ ├── 是 → Serial GC
│ └── 否 ↓
│
├── 对响应时间要求极高(毫秒级)?
│ ├── 是 ↓
│ │ ├── 堆 < 8GB?→ CMS
│ │ └── 堆 > 8GB?→ G1
│ └── 否 ↓
│
├── 业务类型为批处理/后台计算?
│ ├── 是 → Parallel GC
│ └── 否 ↓
│
├── 堆 > 4GB?
│ ├── 是 → G1
│ └── 否 → CMS 或 Parallel GC
2.4 选型约束
| 约束 | 说明 | 影响选型 |
|---|---|---|
| Java 版本 | CMS 在 Java 9 中已废弃,G1 在 Java 9+ 为默认 | Java 8 可用 CMS;Java 9+ 推荐 G1 |
| 容器环境 | 容器内存限制需 JVM 识别 | Java 8u131 之前需手动设置 -Xmx |
| 物理内存 | JVM 堆 + 元空间 + 线程栈 + 直接内存 + 操作系统 | 堆不能超过物理内存的 70-80% |
| CPU 核数 | 并发回收器需要额外的 CPU 资源 | 低配服务器(< 4 核)慎用 CMS/G1 |
2.5 选型验证方法
选型不是一次性决策,需要压测验证:
- 配置选定的 GC 参数
- 使用压测工具(JMeter、Gatling)模拟业务流量
- 采集 jstat -gcutil 数据,重点关注:FGC 是否增长、最大 GC 停顿时间是否满足业务要求、GC 吞吐量是否在可接受范围
- 如果验证不通过,在当前选型基础上微调参数
- 微调仍无效,回到决策矩阵重新选型
三、核心 JVM 参数
3.1 内存容量参数
| 参数 | 含义 | 默认值 | 建议 |
|---|---|---|---|
| -Xms | 初始堆大小 | 物理内存的 1/64 | 与 -Xmx 一致,避免扩容开销 |
| -Xmx | 最大堆大小 | 物理内存的 1/4 | 根据业务需求设定,留 20-30% 给系统 |
| -Xmn | 年轻代大小 | 堆的 1/3 | 根据对象分配速率调整 |
| -XX:NewRatio | 老年代:年轻代 | 2 | 吞吐量优先可调大(如 3),低延迟可调小(如 1) |
| -XX:SurvivorRatio | Eden:Survivor | 8 | 存活率高时调小(如 4),存活率低时调大(如 16) |
| -Xss | 线程栈大小 | 平台相关(通常 1MB) | 高并发时调小(如 256K) |
| -XX:MetaspaceSize | 元空间触发 GC 阈值 | 约 20.8MB | 根据类加载量设置,避免频繁扩容 FGC |
| -XX:MaxMetaspaceSize | 最大元空间 | 不限制(物理内存上限) | 强烈建议设置,避免物理内存耗尽 |
| -XX:MaxDirectMemorySize | 最大直接内存 | 与 -Xmx 一致 | NIO 大量使用时显式设置 |
关于 MetaspaceSize 的说明: 该参数不是"元空间初始大小",而是"触发 FGC 的阈值"。当元空间使用量达到该值时,JVM 会触发 FGC 来回收类元数据。
3.2 GC 行为调优参数(以 CMS 为例)
| 参数 | 含义 | 建议值 |
|---|---|---|
| -XX:CMSInitiatingOccupancyFraction | CMS 触发阈值(老年代占用百分比) | 70-75 |
| -XX:+UseCMSInitiatingOccupancyOnly | 固定使用设定阈值 | 推荐开启 |
| -XX:+CMSClassUnloadingEnabled | 并发回收类元数据 | 推荐开启 |
| -XX:+UseCMSCompactAtFullCollection | FGC 时压缩碎片 | 推荐开启 |
| -XX:CMSFullGCsBeforeCompaction | 多少次 FGC 后压缩 | 0(每次压缩) |
3.3 GC 行为调优参数(以 G1 为例)
| 参数 | 含义 | 建议值 |
|---|---|---|
| -XX:MaxGCPauseMillis | 目标最大 GC 停顿时间 | 200ms |
| -XX:G1HeapRegionSize | 分区大小 | 自动,可设 1/2/4/8/16/32MB |
| -XX:G1NewSizePercent | 年轻代初始比例 | 5% |
| -XX:G1MaxNewSizePercent | 年轻代最大比例 | 60% |
| -XX:G1ReservePercent | 预留空间比例 | 10% |
3.4 诊断与日志参数
| 参数 | 作用 |
|---|---|
| -XX:+PrintGCDetails | 打印 GC 详细信息 |
| -XX:+PrintGCDateStamps | 打印日期时间戳 |
| -XX:+PrintGCTimeStamps | 打印 JVM 启动后相对时间 |
| -XX:+PrintGCApplicationStoppedTime | 打印 STW 暂停时间 |
| -XX:+PrintTenuringDistribution | 打印年龄分布 |
| -Xloggc:/path/to/gc.log | GC 日志输出路径 |
| -XX:+UseGCLogFileRotation | 启用 GC 日志轮转 |
| -XX:NumberOfGCLogFiles=5 | 日志文件数量 |
| -XX:GCLogFileSize=100M | 单个日志文件大小 |
| -XX:+HeapDumpOnOutOfMemoryError | OOM 时导出堆转储 |
| -XX:HeapDumpPath=/path/to/heap.hprof | 堆转储路径 |
| -XX:+DisableExplicitGC | 禁用 System.gc() 显式调用 |
关于 GCLogFileRotation: Java 8 中该功能存在已知 bug(某些版本下日志轮转会丢失日志),在 Java 8u242 之后修复。如使用早期版本,建议使用 logrotate 等外部工具替代。
四、监控与诊断工具(自带)
4.1 命令行工具速查
| 命令 | 用途 | 常用选项 |
|---|---|---|
| jps -l | 查看 Java 进程 PID | -l(显示完整类名) |
| jstat -gcutil 5000 | GC 使用率实时监控 | -gcutil, -gc, -gccause |
| jmap -heap | 查看堆内存配置和摘要 | -heap, -histo:live, -dump |
| jstack -l | 查看线程堆栈 | -l(打印锁信息) |
| jcmd VM.flags | 查看 JVM 所有参数 | VM.flags, GC.heap_info |
| jcmd GC.heap_dump | 导出堆转储 | - |
注意事项:
jmap -histo:live会触发一次 Full GC,生产环境慎用jmap -heap在容器环境中可能受限,建议使用jcmd <pid> GC.heap_info替代
4.2 jstat 输出详解
jstat -gcutil 输出列:
| 列 | 含义 | 健康值 | 异常信号 |
|---|---|---|---|
| S0 | Survivor 0 使用率 | 与 S1 交替为 0 | S0=S1=0 或持续 100% |
| S1 | Survivor 1 使用率 | 与 S0 交替为 0 | 同上 |
| E | Eden 使用率 | 波动,YGC 后降低 | 持续 > 95% 表示 YGC 频繁 |
| O | 老年代使用率 | < 80% | > 80% 需关注,持续增长可能泄漏 |
| M | 元空间使用率 | < 80% | > 80% 需关注 |
| YGC | YGC 次数 | 随运行增长 | 增长过快表示 Eden 太小 |
| YGCT | YGC 总耗时(秒) | 平均 < 50ms | 平均 > 100ms 需关注 |
| FGC | Full GC 次数 | 极少 | 频繁增长是核心问题信号 |
| FGCT | Full GC 总耗时(秒) | 占比 < 10% | > 30% 严重异常 |
| GCT | GC 总耗时(秒) | - | - |
4.3 jstat 异常模式速查
| 现象 | 可能原因 | 验证方法 | 解决方案 |
|---|---|---|---|
| FGC↑ O↑ | 老年代空间不足 | jmap -histo 查看大对象 | 增大 -Xmx 或排查泄漏 |
| FGC↑ O↓ | 晋升失败或 System.gc() | 代码审查 + 查看 GC 日志 | 加 -XX:+DisableExplicitGC;检查 Survivor 配置 |
| FGC↑ M↑ | 元空间不足 | 检查类加载数量 | 增大 -XX:MetaspaceSize |
| YGC↑ E↑ | Eden 太小 | 查看 YGC 间隔 | 增大 -Xmn |
| YGC↑ O↑ | Survivor 太小导致提前晋升 | S0/S1 为 0 | 调整 -XX:SurvivorRatio |
| S0=S1=0 | Survivor 完全失效 | 查看 GC 日志确认提前晋升 | 增大 Survivor 或切换 CMS/G1 |
| 长时间 STW | CMS 并发模式失败 | 查看 GC 日志 | 调低 CMSInitiatingOccupancyFraction |
4.4 jmap -heap 关键输出解读
bash
jmap -heap <pid>
重点关注:
| 输出项 | 含义 | 检查要点 |
|---|---|---|
| MaxHeapSize | -Xmx 是否生效 | 与配置一致 |
| NewRatio | 老年代/年轻代比例 | 是否符合业务 |
| SurvivorRatio | Eden/Survivor 比例 | 是否过小导致晋升 |
| Eden Space used | Eden 实际使用量 | 接近 100% 表示 YGC 频繁 |
| Old Generation used | 老年代实际使用量 | 持续增长可能内存泄漏 |
4.5 GC 日志关键信号
| 日志内容 | 含义 | 响应动作 |
|---|---|---|
| Full GC (Metadata GC Threshold) | 元空间触发 FGC | 增大 MetaspaceSize |
| Full GC (Ergonomics) | JVM 自适应触发 | 检查 -Xmx 是否合理 |
| concurrent mode failure | CMS 并发回收跟不上 | 调低 CMSInitiatingOccupancyFraction |
| Promotion Failed | 晋升失败 | 检查 Survivor 或调大老年代 |
| PSYoungGen: X->0 | 存活对象全部移出年轻代 | 提前晋升,检查 Survivor |
五、常见问题与调优方案
5.1 Full GC 频繁
现象: FGC 次数快速增长,FGCT 占比高
排查步骤:
text
第一步:jstat -gcutil <pid> 5000 确认 FGC 确实频繁
第二步:看 O 列(老年代使用率)
第三步:看 M 列(元空间使用率)
第四步:检查代码中是否有 System.gc()
根因与方案:
| 根因 | 特征 | 方案 |
|---|---|---|
| 老年代空间不足 | O > 80%,持续增长 | 增大 -Xmx;或排查内存泄漏 |
| 晋升失败 | O < 30%,S0/S1=0 | 调大 Survivor 或改用 CMS/G1 |
| System.gc() 显式调用 | 代码审查确认 | 添加 -XX:+DisableExplicitGC |
| 元空间不足 | M > 80% | 增大 -XX:MaxMetaspaceSize |
5.2 YGC 频繁
现象: YGC 次数增长过快,YGCT 占比高
根因: Eden 区太小,对象分配速率过高
方案:
- 增大 -Xmn(年轻代大小)
- 或保持 -Xmn 不变,通过 -XX:NewRatio 降低老年代占比
- 检查是否存在大量短生命周期对象
5.3 GC 停顿时间过长
现象: 单次 GC 耗时超过阈值,业务卡顿
根因与方案:
| 根因 | 方案 |
|---|---|
| Parallel GC Full GC 扫描全堆 | 切换 CMS 或 G1 |
| 堆太大(> 16GB) | 考虑 G1 或 ZGC |
| Survivor 太小导致提前晋升 | 调大 SurvivorRatio |
| CMS 并发模式失败 | 调低 CMSInitiatingOccupancyFraction |
5.4 内存泄漏
现象: 老年代使用率持续增长,FGC 后不下降
排查步骤:
text
1. jmap -histo:live <pid> | head -20 # 查看存活对象统计
2. 多次采样对比,找到持续增长的类型
3. jmap -dump:live,format=b,file=heap.hprof <pid> # 导出堆转储
4. 用 VisualVM 或 MAT 分析堆转储,定位泄漏根因
注意事项: jmap -histo:live 会触发 Full GC,生产环境操作需谨慎。
5.5 CPU 飙高
排查步骤:
text
1. top -H -p <pid> # 找到 CPU 最高的线程 ID(十进制)
2. printf "%x\n" <tid> # 转十六进制
3. jstack <pid> > stack.log # 导出线程堆栈
4. 在堆栈中搜索十六进制 TID # 定位具体代码
5.6 线程死锁
排查:
bash
jstack -l <pid> | grep "Found one Java-level deadlock" -A 20
方案: 根据堆栈信息修复代码中的锁顺序问题
六、调优决策矩阵
6.1 GC 算法选择
text
堆大小 < 4GB?
├── 是 → 响应时间要求高?
│ ├── 是 → CMS
│ └── 否 → Parallel GC
└── 否 → G1GC
注:Java 8u40+ 推荐 G1GC 作为默认选项,适应性最强
6.2 内存分配决策
text
业务类型判断
├── 高并发 Web 应用(大量短生命周期对象)
│ → 年轻代占比 30-40%,SurvivorRatio=4-8
├── 批处理/数据分析(大量长生命周期对象)
│ → 年轻代占比 20-30%,老年代占比 70-80%
└── 混合型应用
→ 年轻代占比 33%(默认),观察 GC 数据后调整
6.3 调优优先级
text
优先级 1(必做):
- 设置 -Xms = -Xmx
- 设置 -XX:+DisableExplicitGC
- 开启 GC 日志
优先级 2(根据场景):
- 选择合适的 GC 算法
- 设置堆大小(不超过物理内存 70%)
- 设置元空间上限(-XX:MaxMetaspaceSize)
优先级 3(精细调优):
- 调整年轻代/老年代比例
- 调整 Eden/Survivor 比例
- 调整 GC 触发阈值
优先级 4(可观测性):
- 接入监控告警系统
- GC 日志自动采集到 ELK
- 设置关键指标告警阈值
七、日志与监控
7.1 推荐 GC 日志配置
bash
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-Xloggc:/data/logs/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M
7.2 容器环境特殊配置
容器环境(Docker/K8s)下 JVM 调优需额外注意:
| 配置项 | 说明 |
|---|---|
| Java 8u131 之前 | 不识别容器内存限制,需手动设置 -Xmx |
| Java 8u191+ | 默认开启 -XX:+UseContainerSupport |
| 建议配置 | 保留 20-30% 内存给非堆区域(元空间、栈、操作系统) |
容器环境 GC 日志配置建议使用 logrotate 替代 UseGCLogFileRotation,避免轮转 bug。
7.3 关键告警阈值
| 指标 | 告警阈值(黄色) | 严重告警(红色) |
|---|---|---|
| FGC 频率 | > 3 次/小时 | > 10 次/小时 |
| Full GC 耗时 | > 1s | > 3s |
| GC 停顿占比 | > 10% | > 30% |
| 老年代使用率 | > 80% | > 90% |
| 元空间使用率 | > 80% | > 90% |
7.4 健康检查脚本
bash
#!/bin/bash
# 快速健康检查脚本
PID=$(jps -l | grep -v Jps | awk '{print $1}')
echo "=== JVM 参数 ==="
jcmd $PID VM.flags
echo ""
echo "=== GC 概况(5秒采样)==="
jstat -gcutil $PID 5000 3
echo ""
echo "=== 堆内存摘要 ==="
jmap -heap $PID | head -30
echo ""
echo "=== 线程状态统计 ==="
jstack $PID | grep "Thread.State" | sort | uniq -c
echo ""
echo "=== 最大对象 TOP 20 ==="
jmap -histo:live $PID | head -20
八、参数速查表
8.1 内存参数
| 参数 | 含义 | 默认值 |
|---|---|---|
| -Xms | 初始堆大小 | 物理内存 1/64 |
| -Xmx | 最大堆大小 | 物理内存 1/4 |
| -Xmn | 年轻代大小 | 堆的 1/3 |
| -Xss | 线程栈大小 | 平台相关 |
| -XX:NewRatio | 老年代:年轻代 | 2 |
| -XX:SurvivorRatio | Eden:Survivor | 8 |
| -XX:MaxTenuringThreshold | 晋升年龄阈值 | 15 |
| -XX:MetaspaceSize | 元空间 GC 阈值 | ~20.8MB |
| -XX:MaxMetaspaceSize | 最大元空间 | 不限制(物理内存上限) |
8.2 GC 参数
| 参数 | 含义 |
|---|---|
| -XX:+UseSerialGC | 串行回收器 |
| -XX:+UseParallelGC | 并行回收器(Java 8 默认,自动启用 Parallel Old) |
| -XX:+UseConcMarkSweepGC | CMS 回收器 |
| -XX:+UseG1GC | G1 回收器 |
| -XX:CMSInitiatingOccupancyFraction | CMS 触发阈值(默认 -1,即 JVM 自动计算) |
| -XX:MaxGCPauseMillis | G1 目标停顿时间 |
| -XX:+DisableExplicitGC | 禁用 System.gc() |
8.3 诊断参数
| 参数 | 含义 |
|---|---|
| -XX:+PrintGCDetails | 详细 GC 日志 |
| -XX:+PrintGCDateStamps | 日期时间戳 |
| -XX:+HeapDumpOnOutOfMemoryError | OOM 导出堆转储 |
| -XX:+PrintGCApplicationStoppedTime | 打印 STW 时间 |
附录:完整配置模板
A. 生产环境推荐配置(G1GC)
bash
# ============================================================
# 通用 Web 应用推荐配置(堆 6-8GB,Java 8u40+)
# ============================================================
# 基础配置
JAVA_OPTS="-Xms8g -Xmx8g -Xss256k"
# 元空间
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1g"
# GC 算法
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=200"
# 禁用显式 GC
JAVA_OPTS="$JAVA_OPTS -XX:+DisableExplicitGC"
# GC 日志
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
JAVA_OPTS="$JAVA_OPTS -Xloggc:/data/logs/gc.log"
JAVA_OPTS="$JAVA_OPTS -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M"
# OOM 堆转储
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/data/logs/heap.hprof"
# 编码
JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8"
B. 生产环境推荐配置(CMS)
bash
# ============================================================
# 低延迟 Web 应用推荐配置(堆 4-8GB,Java 8)
# ============================================================
# 基础配置
JAVA_OPTS="-Xms6g -Xmx6g -Xss256k"
# 元空间
JAVA_OPTS="$JAVA_OPTS -XX:PermSize=512m -XX:MaxPermSize=1g"
# GC 算法
JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC"
JAVA_OPTS="$JAVA_OPTS -XX:+UseParNewGC"
JAVA_OPTS="$JAVA_OPTS -XX:CMSInitiatingOccupancyFraction=70"
JAVA_OPTS="$JAVA_OPTS -XX:+UseCMSInitiatingOccupancyOnly"
JAVA_OPTS="$JAVA_OPTS -XX:+CMSClassUnloadingEnabled"
JAVA_OPTS="$JAVA_OPTS -XX:+UseCMSCompactAtFullCollection"
JAVA_OPTS="$JAVA_OPTS -XX:CMSFullGCsBeforeCompaction=0"
# 禁用显式 GC
JAVA_OPTS="$JAVA_OPTS -XX:+DisableExplicitGC"
# GC 日志
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
JAVA_OPTS="$JAVA_OPTS -Xloggc:/data/logs/gc.log"
# OOM 堆转储
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/data/logs/heap.hprof"
# 编码
JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8"
C. 常用排查命令组合
bash
# 1. 找到目标 PID
jps -l
# 2. GC 实时监控(核心命令)
jstat -gcutil <pid> 5000 10
# 3. GC 容量详情
jstat -gc <pid> 5000 5
# 4. 堆内存摘要
jmap -heap <pid>
# 5. 对象分布(定位内存泄漏)
jmap -histo:live <pid> | head -20
# 6. 线程堆栈(定位死锁/卡顿)
jstack -l <pid>
# 7. 确认 JVM 参数
jcmd <pid> VM.flags
# 8. 导出堆转储(离线分析)
jmap -dump:live,format=b,file=heap.hprof <pid>
D. 调优验证清单
完成调优配置后,建议逐项确认:
| 验证项 | 预期结果 |
|---|---|
| 参数是否生效 | jcmd VM.flags 确认 |
| GC 日志是否正常写入 | ls -la /data/logs/gc.log |
| FGC 是否停止增长 | jstat -gcutil 监控 24 小时,FGC 基本不涨 |
| Survivor 是否正常交替 | S0/S1 交替为 0,不同时为 0 |
| 老年代是否平稳波动 | O 列在 50-70% 间平稳波动 |
| 业务功能是否正常 | 核心业务流程验证通过 |
附录二:调优效果评估方法
完成调优配置后,需要量化评估效果:
| 评估维度 | 对比方法 | 核心指标 |
|---|---|---|
| FGC 频率 | 对比调优前后的 jstat 数据 | FGC 次数增长率 |
| GC 停顿时间 | 对比 GC 日志中的 STW 时间 | 平均/最大停顿时间 |
| GC 吞吐量 | 计算 GC 时间占总时间比例 | GCT / (运行时长) |
| 业务响应时间 | 对比调优前后的业务监控数据 | P99 响应时间 |
| 业务错误率 | 对比调优前后的业务错误日志 | 错误率变化 |
附录三:常见陷阱与误区
| 误区 | 正确理解 |
|---|---|
| 堆越大越好 | 堆过大导致 Full GC 扫描时间长;G1 虽分区回收,但分区数量增多也有开销 |
| CMS 最优秀 | CMS 在 Java 9 中已废弃;G1 在 Java 8u40 后更成熟 |
| 调优就是一劳永逸 | 业务流量变化后需要重新评估调优效果,JVM 调优是持续迭代的过程 |
| 只看 FGC 次数 | FGC 次数少但单次时间长也影响业务,需综合关注 FGCT 和最大停顿时间 |
| 所有参数都要调 | 除 -Xms/-Xmx、GC 算法、元空间上限外,其他参数建议先观察再决定是否调整 |
扩展阅读:
- 实战案例:一次业务高峰期行锁堆积故障的 JVM 调优分析
- Oracle 官方文档:Java Platform, Standard Edition Troubleshooting Guide(