JVM调优实战手册(JDK 8 & 17 双版本对比版)
最后更新:2026-08-11
写在前面
这份手册不是教科书,是面试速查宝典。
我整理了这几年面试中被问到最多的JVM问题,针对 JDK 8 和 JDK 17 两个生产主力版本分别给出优化方案,用最接地气的方式给你讲明白。每个知识点都有「面试怎么说」的话术,直接背就行。
版本说明:JDK 8 仍是存量最大的生产版本(占 50%+),JDK 17 是当前主流 LTS。JDK 8 → 17 的升级过程中,GC 选型、日志框架、参数体系都有显著变化,面试官特别喜欢问"你从 8 升到 17 改了哪些参数"。
记住:面试官不关心你背了多少参数,关心的是你有没有真正解决过生产问题。
1. JVM 内存模型全景图
1.1 内存区域划分
JVM内存分五大块,面试必问:
JDK 8 场景:上图 8:1:1 比例适合 JDK 8 默认的 Parallel 收集器。如果 JDK 8 用 G1 或 CMS,同样无固定比例。
JDK 17 场景:默认 G1 收集器,无固定新生代比例,由 JVM 根据停顿时间目标动态调整,无需手动设置
-Xmn。
1.2 各区域OOM场景
| 区域 | OOM错误 | 常见原因 | 解决方案 |
|---|---|---|---|
| 堆 | java.lang.OutOfMemoryError: Java heap space | 对象太多、内存泄漏、大对象直接进老年代 | 调大-Xmx、排查泄漏、优化代码 |
| 堆(GC效率) | java.lang.OutOfMemoryError: GC overhead limit exceeded | GC消耗>98%时间但回收<2%内存 | 排查泄漏、调大堆、检查大对象 |
| 元空间 | java.lang.OutOfMemoryError: Metaspace | 动态生成大量类(CGLIB、反射) | 调大MetaspaceSize、检查类加载器 |
| 栈 | java.lang.StackOverflowError | 递归太深、死循环调用 | 检查递归出口、增大-Xss |
| 直接内存 | java.lang.OutOfMemoryError: Direct buffer memory | NIO DirectBuffer用太多 | 调整-XX:MaxDirectMemorySize |
1.3 方法区演进:JDK 8 的 PermGen vs JDK 17 的 Metaspace
JDK 8(PermGen→Metaspace):
JDK 8 之前:方法区叫持久代(PermGen),有固定上限,太小容易
PermGen spaceOOMJDK 8 开始:方法区改为元空间(Metaspace),使用本地内存(而非 JVM 堆),不再受
-Xmx限制JDK 8 参数:
-XX:PermSize-XX:MaxPermSize已废弃,改用-XX:MetaspaceSize-XX:MaxMetaspaceSizeJDK 8 坑:默认 Metaspace 无上限,动态生成大量类(CGLIB/反射)时 native 内存可能被打爆,建议显式设置
-XX:MaxMetaspaceSize
JDK 17(Metaspace 优化):
Compressed Class Space:默认开启压缩类空间,最大1GB(可通过
-XX:CompressedClassSpaceSize调整)类卸载优化:改进了类卸载机制,动态生成的类更容易被回收
反射强封装:JDK 17 默认强封装 JDK 内部 API,反射访问受限,需
--add-opens参数
| 对比项 | JDK 8 | JDK 17 |
|---|---|---|
| 默认比例 | 无固定新生代比(Parallel 8:1:1) | G1 动态调整 |
| 压缩类空间 | ❌ 无 | ✅ 默认开启,最大1GB |
| 参数 | MetaspaceSize/MaxMetaspaceSize | 同上 + CompressedClassSpaceSize |
| 反射限制 | 宽松 | 严格(强封装) |
面试怎么说:
"JDK 8 和 17 的方法区都叫 Metaspace,但 17 有几个改进:压缩类空间默认开启,能省 30%-50% 元空间;类卸载机制更智能。JDK 8 的坑是 Metaspace 默认无上限,CGLIB 用多了容易打爆 native 内存,所以 8 上必须设 MaxMetaspaceSize。我们 8 和 17 都设了
-XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1g,比较稳。"
2. GC 算法与收集器选型
2.1 收集器演进史
版本路标:
| 版本 | 默认收集器 | 可选收集器 | 说明 |
|---|---|---|---|
| JDK 8 | Parallel | CMS、G1、Serial | CMS 可用,G1 在 8u40+ 才成熟 |
| JDK 17 | G1 | ZGC、Shenandoah、Parallel、Serial | CMS 已移除,ZGC 生产就绪 |
| JDK 21 | G1 | ZGC(分代)、Shenandoah | 新增虚拟线程、分代 ZGC |
面试重点:JDK 8 默认是 Parallel(吞吐量优先),JDK 17 默认是 G1(延迟优先)。很多人以为 JDK 8 默认 G1,这是常见误区。
2.2 主流收集器对比(含 JDK 8 / 17 可用性)
| 收集器 | 延迟 | 吞吐量 | JDK 8状态 | JDK 17状态 | 适用场景 |
|---|---|---|---|---|---|
| Serial | 高(STW) | 中 | ✅ 可用 | ✅ 可用 | 客户端模式、小堆,不推荐生产 |
| Parallel | 高(STW) | 高 | JDK 8默认 | ✅ 可用 | 批处理、后台计算,吞吐量优先 |
| CMS | 低 | 中 | ✅ 可用(成熟) | ❌ 已移除 | 老系统遗留,JDK 8 可选 |
| G1 | 中(可控) | 中高 | ✅ 可用(8u40+成熟) | JDK 17默认 | 通用场景、中大堆 |
| ZGC | 极低(<1ms) | 中 | ❌ 无 | ✅ 生产就绪 | 低延迟、大堆(>4GB) |
| Shenandoah | 极低(<1ms) | 中 | ❌ 无 | ✅ 生产就绪 | 低延迟、OpenJDK生态 |
JDK 8 关键提醒:CMS 在 JDK 8 仍可用,但已标记废弃。JDK 8 没有 ZGC 和 Shenandoah(JDK 11 才引入)。如果 JDK 8 需要低延迟,只能选 G1 或 CMS。
2.3 G1 vs Parallel(JDK 8 选型核心) vs ZGC(JDK 17 选型核心)
G1 vs Parallel(JDK 8 最纠结的选型):
| 维度 | Parallel(JDK 8 默认) | G1(JDK 8 可选) |
|---|---|---|
| 停顿时间 | 不可控,STW 时间随堆增长 | 可控,100-500ms |
| 吞吐量 | 最高(几乎没有额外开销) | 中高(比 Parallel 低 2%-5%) |
| 调优复杂度 | 低(简单参数) | 中等(需要调 Region、IHOP) |
| 适用场景 | 批处理、夜间任务、不在乎停顿 | 通用应用、对延迟敏感 |
| JDK 8 建议 | 默认即可,简单稳定 | 如果停顿问题 > 吞吐量损失,选 G1 |
G1 vs ZGC(JDK 17 选型核心):
| 维度 | G1(JDK 17 默认) | ZGC(JDK 17 可选) |
|---|---|---|
| 停顿时间 | 100-500ms(可配置) | <1ms(几乎无停顿) |
| 吞吐量 | 高(-2%~-5%) | 中高(-5%~-10%) |
| 内存开销 | 中等 | 较高(需要染色指针) |
| 堆大小 | 4GB-64GB 最佳 | 8GB-16TB(支持超大堆) |
| 算法 | 分区收集+复制算法 | 并发整理+染色指针 |
| 大对象处理 | Humongous 区 | 直接支持,无特殊处理 |
| 调优复杂度 | 中等(需要调参) | 低(几乎开箱即用) |
| JDK 17 差异 | 成熟稳定 | 非分代 ZGC(JDK 21 才有分代 ZGC) |
2.4 JDK 8 vs JDK 17 GC 选型实战指南
JDK 8 场景:
JDK 17 场景:
JDK 8 → 17 升级时 GC 切换要点:
CMS 必须替换:JDK 8 的 CMS 用户,升到 17 必须切到 G1 或 ZGC
Parallel 可保留:JDK 8 的 Parallel 用户,升到 17 仍可用 Parallel,但建议评估 G1
G1 配置需调整:JDK 8 的 G1 参数(如
-XX:G1HeapRegionSize)在 JDK 17 中部分发生了变化ZGC 是新选择:JDK 8 用不了 ZGC,升到 17 后如果延迟敏感,强烈推荐评估
2.5 收集器选型口诀
面试怎么说:
"我们生产有 JDK 8 和 JDK 17 两套。JDK 8 的业务分两类:批处理任务用默认 Parallel,吞吐量最高;在线 API 用 G1,控制停顿在 200ms 内。JDK 17 的新业务统一用 G1,其中延迟敏感的切了 ZGC,P99 从 G1 的 200ms 降到 1ms 以内,吞吐量损失 5% 左右可接受。升级 JDK 17 时最头疼的是 CMS 系统,必须帮老团队迁移到 G1。"
3. JVM 调优参数速查表
3.1 内存参数
| 参数 | 说明 | 推荐值 | JDK 8兼容 | 备注 |
|---|---|---|---|---|
-Xms | 初始堆大小 | 与-Xmx相同 | ✅ | 避免动态扩容开销 |
-Xmx | 最大堆大小 | 物理内存的60%-80% | ✅ | 留足系统内存 |
-Xmn | 新生代大小 | 堆的1/3-1/2 | ✅ | G1/ZGC下不建议设置;JDK 8 Parallel 可设 |
-Xss | 线程栈大小 | 512k-1m | ✅ | 递归深可适当增大 |
-XX:MetaspaceSize | 元空间初始大小 | 256m-512m | ✅ | JDK 8+ 均可用,太小会频繁GC |
-XX:MaxMetaspaceSize | 元空间最大 | 512m-1g | ✅ | JDK 8 强烈建议设置,默认无上限 |
-XX:MaxDirectMemorySize | 直接内存上限 | 与-Xmx相同 | ✅ | NIO场景注意 |
-XX:PermSize | 持久代初始 | ❌ JDK 8 已废弃 | 旧参数 | 用 Metaspace 替代 |
-XX:MaxPermSize | 持久代最大 | ❌ JDK 8 已废弃 | 旧参数 | 用 MaxMetaspaceSize 替代 |
3.2 GC参数
| 参数 | 说明 | JDK 8兼容 | 适用场景 |
|---|---|---|---|
-XX:+UseG1GC | 使用G1收集器 | ✅(8u40+成熟) | 通用场景,JDK 17默认 |
-XX:+UseParallelGC | 使用Parallel收集器 | ✅ JDK 8默认 | 吞吐量优先 |
-XX:+UseConcMarkSweepGC | 使用CMS | ✅ JDK 8可用 | ❌ JDK 14已移除,17不可用 |
-XX:+UseZGC | 使用ZGC | ❌ JDK 8无 | JDK 17低延迟场景 |
-XX:MaxGCPauseMillis | 目标停顿时间 | ✅ G1专用 | 默认200ms |
-XX:G1HeapRegionSize | Region大小 | ✅ | 1MB-32MB,自动计算 |
-XX:InitiatingHeapOccupancyPercent | 触发GC的堆占用率 | ✅ | 默认45%,延迟敏感可调低 |
-XX:ConcGCThreads | 并发GC线程数 | ✅ | 默认CPU核心数的1/4 |
-XX:ParallelGCThreads | STW阶段线程数 | ✅ | 默认CPU核心数 |
-XX:+UseCompressedOops | 压缩对象指针 | ✅ | JDK 8+默认开启 |
3.3 GC日志参数(JDK 8 老式 vs JDK 17 统一日志)
JDK 8(传统方式):
JDK 17(统一日志框架):
JDK 8 → 17 日志升级要点:
-XX:+PrintGCDetails→-Xlog:gc*-Xloggc:gc.log→-Xlog:gc*:file=gc.log-XX:+PrintGCDateStamps→:time标签-XX:+UseGCLogFileRotation→:filecount=,filesize=标签JDK 17 不再支持老参数,直接用就报错,升级时必须改
3.4 参数版本差异速查
JDK 8 可用但 JDK 17 已移除的参数(升级必改):
| 参数 | JDK 8 | JDK 17 | 替代方案 |
|---|---|---|---|
-XX:+UseConcMarkSweepGC | ✅ 可用 | ❌ 移除 | 升级到 G1 或 ZGC |
-XX:+UseParNewGC | ✅ 可用 | ❌ 移除 | G1 自动管理新生代 |
-XX:CMSInitiatingOccupancyFraction | ✅ 可用 | ❌ 移除 | G1 用 -XX:InitiatingHeapOccupancyPercent |
-XX:+UseCMSInitiatingOccupancyOnly | ✅ 可用 | ❌ 移除 | 同上 |
-XX:+PrintGCDetails | ✅ 可用 | ❌ 移除 | -Xlog:gc* |
-XX:+PrintGCDateStamps | ✅ 可用 | ❌ 移除 | -Xlog:gc*:time |
-Xloggc:gc.log | ✅ 可用 | ❌ 移除 | -Xlog:gc*:file=gc.log |
-XX:+UseGCLogFileRotation | ✅ 可用 | ❌ 移除 | :filecount=,filesize= 标签 |
-XX:PermSize / -XX:MaxPermSize | ❌ 已废弃 | ❌ 移除 | 用 MetaspaceSize / MaxMetaspaceSize |
-XX:+UseCompressedOops | ✅ 默认开启 | ✅ 默认开启 | 无需手动设置,仍兼容 |
JDK 17 推荐但 JDK 8 没有的参数:
| 参数 | JDK 8 | JDK 17 | 说明 |
|---|---|---|---|
-XX:+UseZGC | ❌ 无 | ✅ 可用 | JDK 11 才引入 |
-XX:+UseShenandoahGC | ❌ 无 | ✅ 可用 | JDK 12 才引入 |
-XX:+AlwaysPreTouch | ✅ 可用 | ✅ 可用 | 两个版本都有,但 17 效果更好 |
3.5 常用参数速查表(生产环境推荐)
JDK 8 生产配置
JDK 17 生产配置
面试怎么说:
"我们生产环境分 JDK 8 和 JDK 17 两套。JDK 8 的批处理任务用默认 Parallel,吞吐量优先;在线服务用 G1,目标停顿 200ms。JDK 17 的新业务统一 G1,延迟敏感的业务切 ZGC。JDK 8 的 GC 日志用老的 PrintGCDetails 方式,JDK 17 统一用 -Xlog。两个版本都配置了 OOM 自动 dump 和元空间上限。关键参数固化到启动脚本,不允许随意调整。从 8 升 17 时,最麻烦的是改 GC 日志参数和 CMS 迁移。"
4. GC 日志分析实战
4.1 开启GC日志(JDK 8 vs JDK 17)
4.2 G1日志关键指标解读
典型G1 GC日志:
拆解:
GC(15):第15次GCPause Young (Normal):年轻代GC,正常触发2048M->1024M(8192M):GC前2G → GC后1G,堆总大小8G45.678ms:停顿时间45毫秒
关键指标关注点:
停顿时间:是否超过目标(MaxGCPauseMillis)
回收效率:回收比例是否合理(一般>30%)
GC频率:Young GC间隔是否太短(<5秒可能有问题)
Mixed GC:老年代回收是否正常
Full GC:出现就是问题,必须排查
4.3 ZGC日志关键指标解读
典型ZGC日志:
关键指标:
Mark阶段:并发标记耗时(通常<100ms)
Relocate阶段:对象迁移,主要STW时间(<1ms)
触发原因:Proactive(主动)、Allocation Stall(分配阻塞,严重问题)
内存占用:GC前后占比变化
报警阈值:
停顿时间 > 5ms(异常)
出现
Allocation Stall(严重)GC频率 > 1次/秒(可能内存紧张)
4.4 实战案例:一次GC调优的完整过程
场景:电商订单系统,初始 JDK 17 + G1 收集器,堆大小 4G(后增加 JDK 8 版本对比)
现象:
每周出现 2-3 次接口超时(>2 秒)
GC 日志显示 Full GC 频繁(每天 3-5 次)
Full GC 停顿时间 2-5 秒
排查过程:
看GC日志
发现:Full GC 平均 3 秒,最大 5 秒,每天 3-5 次看监控
老年代增长快,每次 Full GC 后回收不到 10%
怀疑内存泄漏或大对象
Dump堆内存
MAT分析
发现
HashMap占用 40% 堆内存追溯发现是本地缓存,没有设置过期策略
代码里用了
HashMap当缓存,数据只增不减
根因
本地缓存设计缺陷,应该用 Caffeine 或 Redis
数据量从 1 万涨到 100 万,内存爆了
解决方案
调优参数
效果
Full GC 从每天 3-5 次 → 0 次
接口超时消失
P99 延迟从 800ms 降到 150ms
预防:
本地缓存必须设置容量和过期时间
监控老年代增长趋势,提前预警
代码Review关注集合类使用
面试怎么说:
"我们之前有个订单系统,每周都会出现几次接口超时。排查发现是G1频繁Full GC,每次停顿2-5秒。通过GC日志和堆dump分析,发现是本地缓存用的HashMap没有限制大小,数据量从1万涨到100万,内存直接爆了。改造后用了Caffeine缓存,设置了容量和过期时间,同时把堆从4G调到6G,Full GC直接降到0,接口超时也消失了。"
5. 内存泄漏排查实战
5.1 排查工具链
| 工具 | 用途 | 优点 | 缺点 |
|---|---|---|---|
| jstat | 实时监控GC和堆内存 | 轻量、实时 | 只能看统计,不能定位 |
| jmap | 生成堆dump | 官方工具 | STW,生产慎用 |
| jcmd | 生成堆dump(推荐) | 比jmap更安全 | 功能单一 |
| Arthas | 在线诊断神器 | 功能强大、无需重启 | 学习成本高 |
| MAT | 分析堆dump | 可视化好、自动检测泄漏 | 大文件分析慢 |
| VisualVM | 综合监控 | 免费、功能全 | 大堆性能差 |
| Async Profiler | 内存分配采样 | 低开销 | 需要分析能力 |
5.2 完整排查链路
详细步骤:
现象
系统运行一段时间后OOM
或:内存持续增长,重启后恢复
确认
隔离
从负载均衡摘掉问题实例
保留现场,不要重启
Dump
分析
定位
看GC Roots引用链
找到持有对象的代码位置
确认是代码bug还是设计问题
修复
修复代码
本地压测验证
灰度发布
验证
监控内存趋势
观察GC频率
运行72小时无问题=修复成功
预防
代码Review
压测覆盖
监控告警
5.3 常见内存泄漏场景
场景1:ThreadLocal未清理
问题代码:
为什么泄漏:
线程池复用线程,ThreadLocal值不会自动清理
每次请求都set,值越来越多
最终OOM
修复:
场景2:静态集合只增不减
问题代码:
修复:
场景3:监听器未注销
问题代码:
修复:
场景4:ClassLoader泄漏
场景:热部署、动态加载类时,旧的ClassLoader没有被回收
排查:
看元空间是否持续增长
MAT分析ClassLoader引用链
修复:
确保自定义ClassLoader实现正确
热部署时显式调用
close()方法
5.4 实战案例
场景:支付系统,运行3天后OOM
排查过程:
jstat监控
发现:老年代每小时增长200MB,Full GC后回收不到5%Dump堆
MAT分析
Leak Suspects报告指出:
byte[]占用60%堆内存追溯引用链:
byte[]→String→HashMap→RequestContext
定位代码
根因
请求上下文用ThreadLocal存储,但没清理
线程池复用,Map只增不减
3天累积,内存爆了
修复
验证
压测24小时
监控老年代稳定在30%,无增长
上线后运行7天无问题
面试怎么说:
"我们支付系统之前出过OOM,运行3天就挂。通过jstat监控发现老年代持续增长,Full GC回收不了。Dump后用MAT分析,发现是ThreadLocal泄漏。请求上下文存在ThreadLocal里,但没清理,线程池复用导致Map越来越大。修复方案是在Filter里统一调用remove(),同时加了监控告警,老年代超过60%就报警。"
6. CPU 飙高排查
6.1 完整排查步骤
6.2 Arthas实战命令
6.3 常见原因及解决方案
原因1:死循环
现象:某个线程CPU 100%,持续不降
排查:
看到:
定位代码:
修复:加退出条件或超时机制
原因2:频繁GC
现象:多个线程CPU高,且都在GC相关方法
排查:
发现:FGC频繁,每秒1-2次
根因:内存不足,对象创建太快
解决:
排查内存泄漏(见第5章)
调大堆内存
优化代码减少对象创建
原因3:锁竞争
现象:大量线程BLOCKED,CPU高但吞吐量低
排查:
或用Arthas:
根因:锁粒度太大或锁持有时间太长
解决:
减小锁粒度(分段锁)
用读写锁替代互斥锁
优化代码减少锁持有时间
6.4 实战案例
场景:秒杀系统,活动开始后CPU飙到90%
排查过程:
top看进程
Java进程CPU 90%top看线程
发现3个线程CPU都在30%jstack分析
看到:定位代码
根因
HashMap在并发读写时可能死循环(JDK 7)或数据丢失(JDK 8+)
秒杀场景并发量高,触发了问题
修复
验证
压测QPS 10000,CPU稳定在40%
上线后无问题
面试怎么说:
"秒杀系统CPU飙高,通过top定位到3个线程CPU 30%,jstack发现都在HashMap.get()。排查发现是并发读写HashMap导致死循环(JDK 7)或数据竞争(JDK 8+)。改成ConcurrentHashMap后问题解决。这个case说明集合的线程安全性很重要,并发场景一定要用ConcurrentHashMap。"
7. Arthas 命令速查表
7.1 全局命令
| 命令 | 用途 | 示例 |
|---|---|---|
help | 查看帮助 | help dashboard |
version | 查看Arthas版本 | version |
cls | 清屏 | cls |
quit | 退出 | quit |
7.2 JVM相关命令
| 命令 | 用途 | 使用场景 |
|---|---|---|
dashboard | 查看系统整体情况 | 快速了解线程、内存、GC状态 |
thread | 查看线程信息 | thread -n 3 看CPU最高的3个线程 |
jvm | 查看JVM信息 | 看JVM参数、类加载统计 |
sysprop | 查看系统属性 | sysprop java.version |
vmoption | 查看/修改JVM诊断参数 | vmoption PrintGC |
heapdump | 生成堆dump | heapdump /tmp/heap.hprof |
gc | 查看GC信息 | 看各代内存使用情况 |
7.3 Class/ClassLoader相关
| 命令 | 用途 | 使用场景 |
|---|---|---|
sc | 搜索类 | sc *OrderService 查找所有OrderService |
sm | 搜索方法 | sm com.example.OrderService * 看所有方法 |
jad | 反编译 | jad com.example.OrderService 看运行时代码 |
dump | dump类字节码 | dump com.example.OrderService |
classloader | 查看类加载器信息 | classloader -t 看加载器树 |
mc | 内存编译 | mc /tmp/Test.java 动态编译 |
redefine | 热更新类 | redefine /tmp/Test.class 线上修复 |
7.4 方法监控/追踪
| 命令 | 用途 | 使用场景 |
|---|---|---|
watch | 监控方法入参/返回值 | watch OrderService createOrder '{params, returnObj}' |
trace | 追踪方法调用链 | trace OrderService createOrder 看耗时 |
stack | 查看方法调用栈 | stack OrderService createOrder 看谁调用的 |
tt | 记录方法调用 | tt -t OrderService createOrder 记录10次调用 |
monitor | 统计方法调用 | monitor -c 5 OrderService createOrder 每5秒统计 |
7.5 实战场景速查
场景1:线上接口慢,定位耗时
场景2:查看方法入参和返回值
场景3:查看方法被谁调用
场景4:线上代码和Git不一致,确认实际代码
场景5:紧急修复,热更新代码
场景6:记录方法调用,事后分析
面试怎么说:
"Arthas是线上排查神器,我最常用的几个命令:dashboard看整体情况,thread定位CPU高的线程,trace追踪方法耗时定位慢点,watch看入参出参排查数据问题,stack看调用链路,jad反编译确认代码版本。之前有个线上bug,用tt命令记录了几次调用,发现传入的userId是null,快速定位到问题。"
8. JDK 8 vs JDK 17 核心差异与面试考点
8.1 GC 选型差异
| 维度 | JDK 8 | JDK 17 |
|---|---|---|
| 默认 GC | Parallel(吞吐量优先) | G1(延迟优先) |
| 可选 GC | CMS、G1 | ZGC、Shenandoah、Parallel |
| CMS 状态 | ✅ 可用(已标废弃) | ❌ 已移除 |
| ZGC 状态 | ❌ 无 | ✅ 生产就绪(非分代) |
| 调优重心 | Parallel 参数 / CMS 参数 | G1 参数 / ZGC 参数 |
面试怎么说:
"JDK 8 默认是 Parallel,很多人误以为是 G1。JDK 8 时代如果延迟敏感,我用 G1 或 CMS;JDK 17 默认 G1,延迟更低。从 8 升 17 最头疼的是 CMS 迁移,必须帮老系统切到 G1,这需要调整参数并压测验证。"
8.2 日志框架差异
| 维度 | JDK 8 | JDK 17 |
|---|---|---|
| GC 日志 | -XX:+PrintGCDetails -Xloggc:gc.log | -Xlog:gc*:file=gc.log |
| JFR(飞行记录) | 商业版(Oracle JDK 才有) | 开源版也有(OpenJDK 内置) |
| jcmd | 功能有限 | GC.heap_dump 等功能全面 |
JDK 8 日志局限性:
不能按标签分类
日志轮转支持差(8u45+ 才支持)
格式固定,解析成本高
JDK 17 日志优势:
按标签分类(
gc,gc+heap,gc+age)原生日志轮转
支持异步输出
面试怎么说:
"JDK 8 的 GC 日志用 PrintGCDetails,生产上我会上日志轮转。JDK 17 统一用 -Xlog,各种标签可以精细控制。从 8 升 17 时,日志参数必须改,老的 PrintGCDetails 在 17 上直接报错。"
8.3 语法特性差异
| 特性 | JDK 8 | JDK 17 |
|---|---|---|
| Lambda / Stream | ✅ 引入(重大特性) | ✅ 增强 |
| Optional | ✅ 引入 | ✅ 完善 |
| Record | ❌ 无 | ✅ 转正 |
| Sealed Classes | ❌ 无 | ✅ 转正 |
| Pattern Matching | ❌ 无 | ✅ Preview→转正(instanceof) |
| Switch 表达式 | ❌ 无 | ✅ 转正 |
| Text Blocks | ❌ 无 | ✅ 转正 |
| 虚拟线程 | ❌ 无 | ❌ 无(JDK 21 才有) |
面试怎么说:
"JDK 8 到 17 最大的语法变化是 Record、Sealed Classes、Pattern Matching 和 Switch 表达式。这些特性让 Java 代码更简洁。JDK 8 的 Lambda 引入是革命性的,JDK 17 的 Record 和 Pattern Matching 让代码表达能力又上了一个台阶。不过虚拟线程是 21 才有的,17 不能用。"
8.4 安全与权限差异
| 维度 | JDK 8 | JDK 17 |
|---|---|---|
| 反射访问 | 宽松(默认可访问) | 严格(强封装) |
| --add-opens | 不需要 | 反射访问内部 API 必须加 |
| Security Manager | 可用 | Deprecated(未来移除) |
| TLS 版本 | TLS 1.0/1.1 仍支持 | 默认只支持 TLS 1.2+ |
JDK 17 反射限制:
面试怎么说:
"JDK 17 默认强封装 JDK 内部 API,反射访问受限。从 8 升 17 时,很多框架会报 IllegalAccessException,需要在启动参数加 --add-opens。Spring Boot 2.7+ 和 3.x 已经适配了,但老的框架可能需要升级版本。"
8.5 面试怎么聊 JDK 版本
Q:你们生产环境用的什么 JDK 版本?
场景 A(JDK 8):我们生产用的是 JDK 8,稳定运行多年。主要用 Parallel 和 G1 收集器,配合 CMS 处理低延迟场景。目前正在评估升级到 JDK 17,主要考虑 GC 和安全性。
场景 B(JDK 17):我们生产用的是 JDK 17,从 JDK 8 升级过来的。主要收益是默认 G1 延迟更低,Record 和 Pattern Matching 让代码更简洁,安全方面也有提升。升级过程主要改了 GC 日志参数和加了 --add-opens。
Q:JDK 8 和 JDK 17 最大的区别是什么?
A:三个关键点:1)GC 差异——JDK 8 默认 Parallel,JDK 17 默认 G1,CMS 在 17 已移除,ZGC 在 17 可用;2)日志差异——8 用 PrintGCDetails,17 用统一 -Xlog 框架;3)语法差异——17 有 Record、Sealed Classes、Pattern Matching、Switch 表达式。反射限制也是升级时的大坑。
Q:从 JDK 8 升级到 JDK 17 遇到什么问题?
A:主要三个问题:1)CMS 依赖——如果用了 CMS,必须切到 G1 或 ZGC,需要调参和压测;2)GC 日志参数——老的 PrintGCDetails 在 17 上不能用,要改成 -Xlog;3)反射访问限制——框架要加 --add-opens,或者升级到适配 17 的版本。依赖兼容性也需要逐个检查。
Q:JDK 8 还值得继续用吗?
A:JDK 8 作为 LTS 版本仍在大量使用,优势是生态成熟、文档齐全、运维经验丰富。劣势是:1)安全漏洞不再有免费补丁(Oracle 需要商业授权);2)没有新特性;3)CMS 已被标记废弃。建议至少评估升级到 JDK 17,如果条件允许,直接上 JDK 21 更好。
9. 面试高频问答
9.1 内存模型相关
Q1:JVM内存结构是什么?
初中级回答:
JVM内存分堆、方法区、栈、本地方法栈、程序计数器。堆是对象分配的地方,栈是方法调用,方法区存类信息。
高级回答:
JVM内存分线程私有和共享两部分。私有的包括程序计数器、虚拟机栈、本地方法栈;共享的包括堆和方法区(JDK 8后是Metaspace)。堆分新生代和老年代,新生代用复制算法,老年代用标记-整理。JDK 17的元空间默认开启压缩类空间,最大1GB,类卸载机制也优化了。
Q2:什么时候会发生OOM?
初中级回答:
对象太多、内存泄漏、堆设置太小都会OOM。
高级回答:
OOM分几种:1)堆溢出,对象太多或内存泄漏;2)元空间溢出,动态生成大量类(CGLIB、反射);3)栈溢出,递归太深;4)直接内存溢出,NIO DirectBuffer用太多。排查思路是先jstat看GC情况,再jmap dump堆,最后MAT分析。
Q3:如何排查内存泄漏?
初中级回答:
用jmap dump堆,然后用MAT分析。
高级回答:
完整排查链路:1)jstat监控,看老年代是否持续增长;2)jmap dump堆,注意生产环境要隔离;3)MAT分析,看Leak Suspects报告;4)追溯GC Roots引用链,找到持有对象的代码;5)修复代码,本地压测验证;6)上线监控,确认问题解决。常见泄漏场景有ThreadLocal未清理、静态集合只增不减、监听器未注销。
9.2 GC相关
Q4:G1和ZGC有什么区别?
初中级回答:
G1停顿时间100-500ms,ZGC<1ms。G1适合通用场景,ZGC适合低延迟场景。
高级回答:
G1 是分区收集+复制算法,停顿时间可控但最低也有几十 ms;ZGC 是并发整理+染色指针,停顿 <1ms。JDK 8 没有 ZGC,低延迟只能选 G1 或 CMS;JDK 17 有 ZGC(非分代),大堆低延迟选 ZGC。选型上,JDK 17:小堆(<4G)用 Parallel,中堆(4-64G)用 G1,大堆(>64G)或对延迟敏感用 ZGC。
Q5:G1的停顿时间能控制到多少?
初中级回答:
可以通过-XX:MaxGCPauseMillis设置,一般设200ms。
高级回答:
MaxGCPauseMillis是目标值,不是保证值。实际停顿取决于堆大小、对象分配速率、并发线程数等。一般设200ms,实际可能在100-500ms之间。如果业务要求<50ms,建议用ZGC。G1的Region大小也会影响停顿,Region越大,单次回收时间越长,但GC频率降低。
Q6:Full GC什么时候触发?如何避免?
初中级回答:
老年代满了、元空间满了、System.gc()都会触发Full GC。避免方法是调大堆、减少对象创建。
高级回答:
Full GC触发条件:1)老年代空间不足;2)元空间不足;3)调用System.gc()(建议型,不保证);4)CMS/G1的并发回收失败(退化为Serial Old)。避免方法:1)合理设置堆大小,老年代预留20%-30%;2)排查内存泄漏;3)优化代码减少对象创建;4)用ZGC避免Full GC(ZGC几乎不会Full GC)。
Q7:GC日志怎么看?
初中级回答:
用-Xlog:gc*开启,看停顿时间、回收前后堆大小。
高级回答:
JDK 17 用统一的
-Xlog框架:-Xlog:gc*:file=gc.log:time,uptime,level,tags。JDK 8 用传统方式:-XX:+PrintGCDetails -Xloggc:gc.log。G1 日志关注:1)停顿时间是否超标;2)回收效率(一般>30%);3)Young GC 间隔(<5秒可能有问题);4)Full GC(出现就是问题)。ZGC 日志关注:1)Mark 和 Relocate 阶段耗时;2)是否出现 Allocation Stall(严重问题);3)GC 频率。
9.3 调优参数相关
Q8:JVM堆大小怎么设置?
初中级回答:
一般设物理内存的50%-80%,-Xms和-Xmx设成一样。
高级回答:
堆大小取决于业务和数据量。一般建议:1)-Xms和-Xmx设成一样,避免动态扩容开销;2)预留20%-30%给系统和其他进程;3)超大堆(>64G)用ZGC;4)小堆(<4G)用Parallel或G1。我们生产环境一般是物理内存的60%-70%,比如16G机器设8-10G。
Q9:-Xmn(新生代大小)怎么设置?
初中级回答:
一般设堆的1/3到1/2。
高级回答:
JDK 8 的 Parallel 场景:设置
-Xmn是有效的,设堆的 1/3 到 1/2,或者用-XX:NewRatio控制。JDK 17 的 G1 场景:不建议设置-Xmn,让 G1 根据MaxGCPauseMillis自动调整新生代大小。强制设置-Xmn可能导致:1)新生代太小,Young GC 频繁;2)新生代太大,老年代空间不足。除非有明确的性能数据支撑,否则让 JVM 自动管理。
Q10:MetaspaceSize怎么设置?
初中级回答:
设256m到512m就行。
高级回答:
MetaspaceSize是初始大小,不是最大大小。建议:1)JDK 8 初始 256m、最大 512m(JDK 8 必须设 MaxMetaspaceSize,否则无上限);2)JDK 17 初始 512m、最大 1g(压缩类空间默认开启,最大 1GB);3)动态生成大量类的应用(如 Spring、CGLIB)适当调大。监控指标是元空间占用率,超过 80% 就要告警。
9.4 排查工具相关
Q11:jstack怎么用?
初中级回答:
jstack <pid> 导出线程栈,看线程在干什么。
高级回答:
jstack用法:1)
jstack <pid>导出所有线程栈;2)jstack -l <pid>包含锁信息;3)jstack -F <pid>强制dump(线程hang住时用)。分析时关注:1)线程状态(RUNNABLE/BLOCKED/WAITING);2)死锁(Found one Java-level deadlock);3)CPU高的线程(结合top -Hp)。
Q12:Arthas和jstack有什么区别?
初中级回答:
Arthas功能更强,可以在线修改代码,jstack只能看线程栈。
高级回答:
jstack是官方工具,只能导出线程快照,分析靠人工。Arthas是阿里开源的在线诊断工具,功能包括:1)dashboard看整体情况;2)thread看线程;3)trace追踪方法耗时;4)watch看入参出参;5)stack看调用链;6)jad反编译;7)redefine热更新。Arthas是交互式工具,可以实时诊断,jstack是一次性快照。生产排查优先用Arthas。
Q13:MAT怎么分析堆dump?
初中级回答:
打开heap.hprof,看Leak Suspects报告。
高级回答:
MAT分析步骤:1)打开heap.hprof(大文件可能需要增大MAT内存);2)选择"Leak Suspects Report",自动检测可疑泄漏;3)看Dominator Tree,找占用内存最大的对象;4)右键"List Objects"→"With outgoing references",看引用链;5)追溯GC Roots,找到持有对象的代码。关键技巧:1)用OQL查询特定对象;2)用Histogram看对象统计;3)对比两个dump,找增长最快的对象。
9.5 JDK 版本对比相关
Q14:JDK 8 和 JDK 17 的默认 GC 分别是什么?为什么不一样?
初中级回答:
JDK 8 默认 Parallel,JDK 17 默认 G1。因为 JDK 8 更关注吞吐量,JDK 17 更关注延迟。
高级回答:
JDK 8 默认 Parallel GC,因为当时的主流场景是批处理和后台计算,吞吐量优先。JDK 17 默认 G1,因为随着互联网应用发展,延迟变得越来越重要。G1 的分区收集 + 可配置停顿时间,能更好地满足在线服务的延迟要求。常见误区:很多人以为 JDK 8 默认是 G1,其实是 Parallel。JDK 9 起才改为 G1 默认。
Q15:JDK 8 的 CMS 在 JDK 17 中怎么替代?
初中级回答:
JDK 17 没有 CMS,必须换成 G1 或 ZGC。
高级回答:
CMS 在 JDK 14 已移除,JDK 17 肯定不能用。替代方案:1)G1(推荐)——大多数场景直接替换,调整
-XX:MaxGCPauseMillis和-XX:InitiatingHeapOccupancyPercent;2)ZGC(低延迟场景)——停顿 <1ms,但需要更多内存,且 JDK 17 只有非分代版本;3)Parallel(不推荐)——如果对延迟完全不敏感。迁移时需要压测对比,确保新 GC 的吞吐量和延迟满足要求。
Q16:JDK 8 升级到 JDK 17 的完整 checklist 是什么?
高级回答:
1)GC 检查:CMS → G1/ZGC,修改 GC 参数;2)日志参数:
-XX:+PrintGCDetails→-Xlog:gc*;3)反射访问:加--add-opens参数,或升级框架版本;4)依赖兼容性:检查所有依赖是否支持 JDK 17;5)编译和测试:JDK 17 编译,全量回归测试;6)JVM 参数:逐条检查 JDK 8 参数在 17 上是否可用;7)灰度发布:先切少量实例观察 3-7 天,确认无问题再全量切换。
9.6 实战场景相关
Q17:线上接口慢,怎么排查?
初中级回答:
看日志,看监控,找慢的地方。
高级回答:
完整排查链路:1)看监控(QPS、延迟、错误率),确认是普遍慢还是个例;2)看GC日志,是否有频繁GC;3)用Arthas trace追踪方法耗时,定位慢点;4)看线程栈(thread命令),是否有锁竞争;5)看数据库慢查询、外部调用超时;6)看系统资源(CPU、内存、磁盘IO、网络)。根据定位结果优化代码、SQL或基础设施。
Q18:CPU飙高怎么排查?
初中级回答:
用top看哪个进程CPU高,然后jstack看线程栈。
高级回答:
完整步骤:1)
top -c找CPU高的进程PID;2)top -Hp <pid>找CPU高的线程TID;3)printf "%x\n" <tid>转16进制;4)jstack <pid> | grep -A 30 "nid=0x..."找对应线程栈;5)定位代码,常见原因:死循环、频繁GC、锁竞争。或用Arthas:dashboard看整体,thread -n 3看CPU最高的3个线程。
Q19:如何预防OOM?
初中级回答:
设置合理的堆大小,避免内存泄漏。
高级回答:
预防措施:1)设置合理的堆大小和GC参数;2)本地缓存必须有容量和过期策略(用Caffeine);3)ThreadLocal必须在finally里remove;4)监控内存趋势,老年代超过60%告警;5)代码Review关注集合类使用、资源释放;6)压测覆盖,提前发现问题;7)配置OOM时自动dump,方便事后排查。
Q20:讲一个你解决过的JVM问题?
高级回答模板:
"我们之前有个XX系统,运行一段时间后会出现XX问题(现象)。通过XX工具排查,发现是XX原因(根因)。修复方案是XX(解决)。修复后XX指标从XX降到XX(效果)。预防后续再发生,我们加了XX监控/改了XX规范(预防)。"
示例:
"我们支付系统之前运行3天就OOM。通过jstat监控发现老年代持续增长,Full GC回收不了。Dump后用MAT分析,发现是ThreadLocal泄漏。请求上下文存在ThreadLocal里没清理,线程池复用导致Map越来越大。修复方案是在Filter里统一调用remove(),同时加了监控告警,老年代超过60%就报警。修复后运行稳定,再没出过OOM。"
写在最后
JVM调优不是背参数,是理解原理+实战经验。
面试官问JVM问题,本质是想看:
你懂不懂原理(内存模型、GC算法)
你分不分得清版本(JDK 8 vs 17 的 GC、日志、参数差异)
你用过什么工具(jstat、jmap、Arthas、MAT)
你解决过什么问题(实战案例)
你能不能预防问题(监控、规范)
JDK 8 + JDK 17 双版本面试小贴士:
先把 JDK 8(默认 Parallel)和 JDK 17(默认 G1)的差异记牢,这是高频考点
背熟 GC 日志在两套版本下的写法差异
准备一个"JDK 8 升 17"的实战叙述(CMS 迁移、--add-opens、日志参数改造)
实战案例要能讲清在哪个版本下发生的,用了什么工具
准备面试时,重点准备 3-5 个实战案例,每个都要能讲清楚:
现象是什么
怎么排查的
根因是什么
怎么解决的
怎么预防的
参数可以忘,工具可以查文档,但排查思路和实战经验是你的核心竞争力。
祝面试顺利!