适用读者:准备中高级 Java 面试、要在生产上处理 GC 抖动或内存泄漏的人。
前置:S04(JVM 基础)、S25-J07(并发题库)、J05(原理速查)。
覆盖旧文:2776/2777/2780+3036–3053(JVM 调优教程 18 章)→ 18 篇 / 约 10 万字符。
扩展点:E-S25-024(收集器选型决策表)、E-S25-025(诊断四件套与"先现象后原因")、E-S25-026(版本差异:JDK 8/17/21 默认收集器)。
上一篇:S25-J07 · 并发题库讲评:从 JUC 二十章到线程池十四问
下一篇:题库层第 3 篇 · MySQL 题库讲评(待写)。
1. 一句话结论
JVM 调优不是"调参数",是"用数据定位瓶颈,再决定要不要动 JVM"。 面试的标准答法是:现象 → 指标 → 工具 → 根因 → 验证;只说"加大堆内存"是零分回答。
出处:JVM 调优教程 18 章(
A-3036–A-3053)+A-2777/A-2780同题重文。
2. 收集器选型决策表(E-S25-024)
| 收集器 | 分代 | 线程 | 算法 | 目标 | 适用 |
|---|---|---|---|---|---|
| Serial | 新生代 | 单线程 STW | 复制 | 简单 | 客户端、几十~一两百 MB 堆、单核 |
| Parallel Scavenge / Old | 分代 | 多线程 STW | 复制 / 标记-整理 | 吞吐量优先 | 批处理、后台运算、不敏感停顿 |
| CMS | 老年代 | 并发 | 标记-清除 | 低延迟 | 历史选型(JDK 14 移除) |
| G1 | 整堆(Region) | 并发 | 复制+标记-整理 | 可预测停顿 | 大堆(>4 GB)默认选择 |
| ZGC | 整堆 | 并发 | 染色指针+读屏障 | 超低停顿(<1 ms 级) | 超大堆、极低延迟要求 |
| Shenandoah | 整堆 | 并发 | Brooks 转发指针 | 低停顿 | 非 Oracle JDK 生态常见 |
版本边界(E-S25-026,必答且常被追问):
| JDK | 默认 GC | 备注 |
|---|---|---|
| 8 | Parallel(Server 模式) | 需手动 -XX:+UseG1GC 才用 G1 |
| 9–10 | G1 | JEP 248 起默认 |
| 11 | G1 | ZGC 实验性(JEP 333) |
| 14 | G1 | CMS 移除(JEP 363) |
| 15 | G1 | 偏向锁默认禁用(JEP 374) |
| 17 | G1 | ZGC 生产可用(JEP 377) |
| 21 | G1 | 分代 ZGC(JEP 439)、虚拟线程(JEP 444) |
判据:先问"你的 JDK 是几",再谈默认收集器。 答"默认 Parallel"在 JDK 11 上会被直接判错。
3. 诊断四件套与"先现象后原因"(E-S25-025)
top -Hp <pid> # 1. 哪个线程吃 CPU(拿 nid)
printf "%x\n" <nid> # 2. 转 16 进制
jstack <pid> > dump.txt # 3. 抓栈,grep -A20 <nid>
jstat -gcutil <pid> 1000 # 4. GC 频率与各代占用
jmap -histo:live <pid> | head # 5. 对象直方图(看谁在涨)
jcmd <pid> VM.flags # 6. 实际生效参数(别信启动脚本)
诊断六原则(原文第 17 章):先整体后局部 / 先现象后原因 / 先简单后复杂 / 数据驱动 / 可重现验证 / 总结经验。
3.1 四类问题的现场手法
| 现象 | 首要怀疑 | 手法 |
|---|---|---|
| CPU 高 | 死循环、正则回溯、频繁 GC、锁自旋 | top -Hp → jstack → 找热点方法;arthas profiler |
| 响应慢 | GC 停顿、线程阻塞、IO 等待、慢 SQL | jstat -gc 看停顿;jstack 看 BLOCKED/WAITING;APM 链路 |
| 内存涨 | 泄漏(静态集合、ThreadLocal、连接未关) | jmap -histo:live 两次对比;jcmd GC.heap_dump 出 dump 用 MAT |
| OOM | 堆 / 元空间 / 直接内存 / 线程栈 | 看错误文本:Java heap space / Metaspace / Direct buffer memory / unable to create native thread |
判据:OOM 的四种类型对应四种不同根因,答"加大 -Xmx"只在第一种情况成立。
4. 三套实战:按版本与场景分
4.1 JDK 8:Parallel 到 G1 的迁移评估
# 老环境(JDK 8 + Parallel,堆 8G)
java -Xms8g -Xmx8g -XX:+UseParallelGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-Xloggc:/var/log/app/gc.log -jar app.jar
# 评估:单次 Full GC 是否 > 1s、老年代晋升速率
迁移判据:堆 > 4 GB、Full GC 停顿 > 500 ms、且应用对尾延迟敏感 → 试 G1;改 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 后对比 p99 而非均值。
4.2 JDK 17:G1 参数的最小集
java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=8m -XX:+ParallelRefProcEnabled \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags -jar app.jar
要点:-Xms=-Xmx 避免堆伸缩;MaxGCPauseMillis 是软目标(G1 会尽力,不保证);-Xlog 是新版统一日志,旧的 PrintGCDetails 在 JDK 9+ 已不生效。
4.3 JDK 21:分代 ZGC 与虚拟线程
# 极低延迟(金融/网关),大堆
java -Xms16g -Xmx16g -XX:+UseZGC -XX:+ZGenerational \
-Xlog:gc*:file=/var/log/app/zgc.log -jar app.jar
边界:ZGC 的代价是吞吐略低 + 内存占用更高(染色指针/转发表);虚拟线程适合 IO 密集,但 synchronized 块会 pin 住载体线程(JDK 21),密集锁场景仍应评估平台线程。
5. 高频题速查:答准而不是答多
| 题 | 关键点 |
|---|---|
| 内存结构 | 堆、方法区/元空间、虚拟机栈、本地方法栈、程序计数器、直接内存(不在 JVM 规范内) |
| 对象一定在堆上吗 | 逃逸分析后可栈上分配/标量替换(是优化不是保证) |
| 什么时候进老年代 | 年龄达阈值、大对象直接进、动态年龄判定(Survivor 同龄和 > 一半) |
| Full GC 触发条件 | 老年代不足、元空间不足、System.gc()(受 -XX:+DisableExplicitGC 影响)、G1 的并发模式失败 |
| 类加载双亲委派 | 防核心类被篡改;打破场景:SPI(Thread.contextClassLoader)、OSGi、热部署 |
| JIT 什么时候生效 | 解释执行 → C1 → C2;-XX:+PrintCompilation 观察;峰值性能要预热 |
| 怎么判断内存泄漏 | 老年代占用持续上升且 Full GC 后不回落 + jmap -histo:live 两次对比 |
6. 监控体系:分层而不是堆工具
| 层 | 指标 | 工具 |
|---|---|---|
| 基础 | CPU/内存/磁盘/网络 | node_exporter |
| JVM | 堆/非堆、GC 次数与停顿、线程数、类加载 | jmx_exporter + /actuator/prometheus |
| 应用 | RT、QPS、错误率 | Micrometer / SkyWalking |
| 业务 | 关键流程成功率 | 自定义埋点 |
# prometheus.yml 采集 JVM 指标(Spring Boot 3 + Actuator)
scrape_configs:
- job_name: 'jvm-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
要点:heap_used 要配 jvm_gc_pause_seconds_max 一起看——只看堆占用会误判(G1 下堆高位但停顿正常是健康状态)。
7. 反例:四种"看起来专业"的错答
| 答法 | 问题 |
|---|---|
| "调优就是把 Xmx 调到物理内存 80%" | 无前提;容器里还要看 cgroup limit |
| "G1 一定比 Parallel 快" | 吞吐通常不如 Parallel;选型看目标(延迟 vs 吞吐) |
| "出现 OOM 就加堆" | 元空间/直接内存/线程栈三类 OOM 加堆无效 |
| "CMS 是最佳低延迟方案" | JDK 14 已移除;应说 G1/ZGC |
8. 自测
- JDK 8 / 11 / 17 / 21 的默认收集器分别是什么?
- CPU 飙高的六步定位流程?
- 四种 OOM 的错误文本与根因?
- 为什么
-Xlog:gc*取代了-XX:+PrintGCDetails?
答案
- Parallel / G1 / G1 / G1(JDK 21 另有分代 ZGC 可选)。
top -Hp→printf %x→jstack→grep -A20 nid→ 定位方法 → 结合jstat排除 GC。Java heap space(堆)、Metaspace(元空间)、Direct buffer memory(直接内存)、unable to create native thread(线程/栈)。- JDK 9 起统一日志框架(JEP 158),旧参数失效或被映射,粒度控制更细(tags/level/output)。
9. 下一篇
题库层第 3 篇 · MySQL 题库讲评 —— 索引与 B+ 树、事务与 MVCC、锁升级与临键锁、redo/undo 与两阶段提交、主从延迟,以及"分库分表后分页怎么查"这类场景题的标准答法。
本篇属于系列《S25 · 求职与面试》。旧文来源:JVM 调优教程 18 章(A-3036…3053)+ A-2777/2780;扩展点 E-S25-024/025/026。技术细节点以 duplicate_ref 指回 S04/S11 主讲系列;本文只给答题骨架与边界。