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内存分五大块,面试必问:

┌─────────────────────────────────────────────────────┐
│                    JVM 内存结构                      │
├─────────────────────────────────────────────────────┤
│  线程私有:                                          │
│  ├─ 程序计数器(PC Register)                        │
│  ├─ 虚拟机栈(VM Stack)                            │
│  └─ 本地方法栈(Native Method Stack)                │
│                                                      │
│  线程共享:                                          │
│  ├─ 堆(Heap)                                      │
│  │   ├─ 新生代(Young Generation)                  │
│  │   │   ├─ Eden区(80%)                           │
│  │   │   ├─ Survivor0(10%)                        │
│  │   │   └─ Survivor1(10%)                        │
│  │   └─ 老年代(Old Generation)                    │
│  │                                                  │
│  └─ 方法区(Metaspace,JDK 8+)                     │
└─────────────────────────────────────────────────────┘

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 exceededGC消耗>98%时间但回收<2%内存排查泄漏、调大堆、检查大对象
元空间java.lang.OutOfMemoryError: Metaspace动态生成大量类(CGLIB、反射)调大MetaspaceSize、检查类加载器
java.lang.StackOverflowError递归太深、死循环调用检查递归出口、增大-Xss
直接内存java.lang.OutOfMemoryError: Direct buffer memoryNIO DirectBuffer用太多调整-XX:MaxDirectMemorySize

1.3 方法区演进:JDK 8 的 PermGen vs JDK 17 的 Metaspace

JDK 8(PermGen→Metaspace)

  • JDK 8 之前:方法区叫持久代(PermGen),有固定上限,太小容易 PermGen space OOM

  • JDK 8 开始:方法区改为元空间(Metaspace),使用本地内存(而非 JVM 堆),不再受 -Xmx 限制

  • JDK 8 参数-XX:PermSize -XX:MaxPermSize 已废弃,改用 -XX:MetaspaceSize -XX:MaxMetaspaceSize

  • JDK 8 坑:默认 Metaspace 无上限,动态生成大量类(CGLIB/反射)时 native 内存可能被打爆,建议显式设置 -XX:MaxMetaspaceSize

JDK 17(Metaspace 优化)

  • Compressed Class Space:默认开启压缩类空间,最大1GB(可通过 -XX:CompressedClassSpaceSize 调整)

  • 类卸载优化:改进了类卸载机制,动态生成的类更容易被回收

  • 反射强封装:JDK 17 默认强封装 JDK 内部 API,反射访问受限,需 --add-opens 参数

对比项JDK 8JDK 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 收集器演进史

Serial(单线程)
  ↓
Parallel(多线程,JDK 8默认)
  ↓
CMS(并发标记清除,JDK 14移除)
  ↓
G1(分区收集,JDK 9起默认,JDK 17默认)
  ↓
ZGC / Shenandoah(超低延迟,JDK 17生产就绪)

版本路标

版本默认收集器可选收集器说明
JDK 8ParallelCMS、G1、SerialCMS 可用,G1 在 8u40+ 才成熟
JDK 17G1ZGC、Shenandoah、Parallel、SerialCMS 已移除,ZGC 生产就绪
JDK 21G1ZGC(分代)、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 场景

┌─ 业务场景 ──────────────────────────────────┐
│                                              │
│  吞吐量优先(批处理、大数据)→ Parallel       │
│  延迟敏感(Web应用、API)   → G1             │
│  低延迟遗留系统(CMS)      → CMS(迁移中)   │
│  小堆(<4GB)               → Parallel 即可   │
│                                              │
└──────────────────────────────────────────────┘

JDK 17 场景

┌─ 业务场景 ──────────────────────────────────┐
│                                              │
│  通用场景                    → G1(默认)     │
│  低延迟(<10ms)             → ZGC           │
│  超大堆(>64GB)             → ZGC           │
│  吞吐量优先(批处理)        → Parallel      │
│                                              │
└──────────────────────────────────────────────┘

JDK 8 → 17 升级时 GC 切换要点

  1. CMS 必须替换:JDK 8 的 CMS 用户,升到 17 必须切到 G1 或 ZGC

  2. Parallel 可保留:JDK 8 的 Parallel 用户,升到 17 仍可用 Parallel,但建议评估 G1

  3. G1 配置需调整:JDK 8 的 G1 参数(如 -XX:G1HeapRegionSize)在 JDK 17 中部分发生了变化

  4. ZGC 是新选择:JDK 8 用不了 ZGC,升到 17 后如果延迟敏感,强烈推荐评估

2.5 收集器选型口诀

JDK 8:
  吞吐量优先         → Parallel(默认)
  延迟敏感、中堆     → G1
  遗留CMS系统        → 尽早迁移 G1

JDK 17:
  通用场景           → G1(默认)
  低延迟(<10ms)    → ZGC
  超大堆(>64GB)    → ZGC

面试怎么说

"我们生产有 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/2G1/ZGC下不建议设置;JDK 8 Parallel 可设
-Xss线程栈大小512k-1m递归深可适当增大
-XX:MetaspaceSize元空间初始大小256m-512mJDK 8+ 均可用,太小会频繁GC
-XX:MaxMetaspaceSize元空间最大512m-1gJDK 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使用CMSJDK 8可用❌ JDK 14已移除,17不可用
-XX:+UseZGC使用ZGCJDK 8无JDK 17低延迟场景
-XX:MaxGCPauseMillis目标停顿时间✅ G1专用默认200ms
-XX:G1HeapRegionSizeRegion大小1MB-32MB,自动计算
-XX:InitiatingHeapOccupancyPercent触发GC的堆占用率默认45%,延迟敏感可调低
-XX:ConcGCThreads并发GC线程数默认CPU核心数的1/4
-XX:ParallelGCThreadsSTW阶段线程数默认CPU核心数
-XX:+UseCompressedOops压缩对象指针JDK 8+默认开启

3.3 GC日志参数(JDK 8 老式 vs JDK 17 统一日志)

JDK 8(传统方式)

# 基础GC日志(JDK 8 标准写法)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

# 带GC前后堆信息
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log

# 带GC原因
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -Xloggc:gc.log

# 日志轮转(JDK 8u45+ 才支持)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50m

JDK 17(统一日志框架)

# 基础GC日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=50m

# 详细GC日志(调试用)
-Xlog:gc*,gc+heap=debug,gc+age=trace*:file=gc.log:time,uptime,level,tags

# 实时输出到控制台
-Xlog:gc*:stdout:time,uptime,level,tags

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 8JDK 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 8JDK 17说明
-XX:+UseZGC❌ 无✅ 可用JDK 11 才引入
-XX:+UseShenandoahGC❌ 无✅ 可用JDK 12 才引入
-XX:+AlwaysPreTouch✅ 可用✅ 可用两个版本都有,但 17 效果更好

3.5 常用参数速查表(生产环境推荐)

JDK 8 生产配置

# === 内存配置 ===
-Xms4g -Xmx4g                    # 堆大小固定,避免扩容
-Xss512k                         # 线程栈512k
-XX:MetaspaceSize=256m           # 元空间初始256m
-XX:MaxMetaspaceSize=512m        # ⚠️ JDK 8 必须设,防止元空间无限增长

# === GC配置(二选一)===

# 方案A:Parallel(吞吐量优先,JDK 8默认)
-XX:+UseParallelGC
-XX:+UseParallelOldGC            # 老年代并行(JDK 8默认配套)
-XX:ParallelGCThreads=4          # 与CPU核心数一致
-XX:+UseAdaptiveSizePolicy       # 自动调整新生代大小(默认开启)

# 方案B:G1(延迟敏感场景)
-XX:+UseG1GC                     # 使用G1
-XX:MaxGCPauseMillis=200         # 目标停顿200ms
-XX:G1HeapRegionSize=4m          # Region大小(4MB-8MB合适)
-XX:ParallelGCThreads=4          # STW阶段线程数

# === GC日志(JDK 8 传统方式)===
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -Xloggc:gc.log
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100m

# === OOM时自动dump ===
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof

# === 其他 ===
-XX:+UseStringDeduplication      # G1字符串去重,节省内存(8u20+)

JDK 17 生产配置

# === 内存配置 ===
-Xms8g -Xmx8g                    # 堆大小固定,避免扩容
-Xss512k                         # 线程栈512k
-XX:MetaspaceSize=512m           # 元空间初始512m
-XX:MaxMetaspaceSize=1g          # 元空间最大1g

# === GC配置(三选一)===

# 方案A:G1(通用场景,JDK 17默认)
-XX:+UseG1GC                     # 使用G1(可省略,默认就是G1)
-XX:MaxGCPauseMillis=200         # 目标停顿200ms
-XX:G1ReservePercent=10          # 预留10%防to-space exhausted

# 方案B:ZGC(低延迟场景)
-XX:+UseZGC                      # 使用ZGC(⚠️ JDK 17 非分代版本)
-XX:SoftMaxHeapSize=6g           # 软限制,ZGC会尽量遵守

# 方案C:Parallel(吞吐量优先)
-XX:+UseParallelGC               # 批处理场景

# === GC日志(JDK 17 统一日志)===
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=100m

# === OOM时自动dump ===
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof

# === 其他 ===
-XX:+UseStringDeduplication      # G1字符串去重,节省内存
-XX:+AlwaysPreTouch              # 启动时预分配内存,减少首次访问延迟

面试怎么说

"我们生产环境分 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)

# ===== JDK 8 传统方式 =====
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# 参数说明
# -XX:+PrintGCDetails       - 打印GC详细日志
# -XX:+PrintGCDateStamps    - 打印日期时间戳
# -Xloggc:gc.log            - 输出到文件
# 轮转需加:-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50m

# ===== JDK 17 统一日志 =====
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=50m
# 参数说明
# gc*              - 所有GC相关日志
# file=gc.log      - 输出到文件
# time             - 打印时间戳
# uptime           - 打印JVM启动后的运行时间
# level            - 日志级别
# tags             - 标签(gc、heap等)
# filecount=5      - 保留5个文件
# filesize=50m     - 每个文件最大50M

4.2 G1日志关键指标解读

典型G1 GC日志

[2026-08-10T10:30:15.123+0800][12345.678s][info][gc] GC(15) Pause Young (Normal) (G1 Evacuation Pause) 2048M->1024M(8192M) 45.678ms

拆解

  • GC(15):第15次GC

  • Pause Young (Normal):年轻代GC,正常触发

  • 2048M->1024M(8192M):GC前2G → GC后1G,堆总大小8G

  • 45.678ms:停顿时间45毫秒

关键指标关注点

  1. 停顿时间:是否超过目标(MaxGCPauseMillis)

  2. 回收效率:回收比例是否合理(一般>30%)

  3. GC频率:Young GC间隔是否太短(<5秒可能有问题)

  4. Mixed GC:老年代回收是否正常

  5. Full GC:出现就是问题,必须排查

4.3 ZGC日志关键指标解读

典型ZGC日志

[2026-08-10T10:30:15.123+0800][12345.678s][info][gc] GC(15) Garbage Collection (Proactive)
[2026-08-10T10:30:15.124+0800][12345.679s][info][gc] GC(15) GC(15) Mark Start
[2026-08-10T10:30:15.234+0800][12345.789s][info][gc] GC(15) GC(15) Mark End (100ms)
[2026-08-10T10:30:15.235+0800][12345.790s][info][gc] GC(15) GC(15) Relocate Start (1ms)
[2026-08-10T10:30:15.345+0800][12345.900s][info][gc] GC(15) Garbage Collection (Proactive) 2048M(25%) -> 1024M(12%)

关键指标

  1. Mark阶段:并发标记耗时(通常<100ms)

  2. Relocate阶段:对象迁移,主要STW时间(<1ms)

  3. 触发原因:Proactive(主动)、Allocation Stall(分配阻塞,严重问题)

  4. 内存占用: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 秒

排查过程

  1. 看GC日志

    # JDK 17 方式
    grep "Pause Full" gc.log | awk '{print $NF}' | sort -n | tail -5
    
    # JDK 8 方式(日志格式不同)
    grep "Full GC" gc.log | awk '{print $NF}' | sort -n | tail -5
    

    发现:Full GC 平均 3 秒,最大 5 秒,每天 3-5 次

  2. 看监控

    • 老年代增长快,每次 Full GC 后回收不到 10%

    • 怀疑内存泄漏或大对象

  3. Dump堆内存

    # JDK 8 推荐
    jmap -dump:format=b,file=heap.hprof <pid>
    
    # JDK 17 推荐(jcmd 更安全)
    jcmd <pid> GC.heap_dump /data/logs/heap.hprof
    

  4. MAT分析

    • 发现 HashMap 占用 40% 堆内存

    • 追溯发现是本地缓存,没有设置过期策略

    • 代码里用了 HashMap 当缓存,数据只增不减

  5. 根因

    • 本地缓存设计缺陷,应该用 Caffeine 或 Redis

    • 数据量从 1 万涨到 100 万,内存爆了

  6. 解决方案

    // 改造前(问题代码,JDK 8 和 17 都一样)
    Map<String, Order> cache = new HashMap<>();
    
    // 改造后(使用Caffeine,版本无关)
    Cache<String, Order> cache = Caffeine.newBuilder()
        .maximumSize(100_000)
        .expireAfterWrite(Duration.ofMinutes(30))
        .build();
    

  7. 调优参数

    # ===== JDK 8 版本 =====
    # 调整前
    -Xms4g -Xmx4g -XX:+UseG1GC
    # 调整后
    -Xms6g -Xmx6g -XX:+UseG1GC
    -XX:MaxGCPauseMillis=200
    -XX:InitiatingHeapOccupancyPercent=40  # 提前触发GC
    # 日志也要加
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
    
    # ===== JDK 17 版本 =====
    # 调整前
    -Xms4g -Xmx4g
    # 调整后
    -Xms6g -Xmx6g
    -XX:MaxGCPauseMillis=200
    -XX:InitiatingHeapOccupancyPercent=40
    # 日志用统一格式
    -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=50m
    

  8. 效果

    • 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 完整排查链路

现象 → 确认 → 隔离 → Dump → 分析 → 定位 → 修复 → 验证 → 预防

详细步骤

  1. 现象

    • 系统运行一段时间后OOM

    • 或:内存持续增长,重启后恢复

  2. 确认

    # 看GC情况
    jstat -gcutil <pid> 1000 10  # 每秒输出一次,共10次
    
    # 关注指标
    # O:老年代占用率(持续增长=有问题)
    # M:元空间占用率
    # YGCT/FGCT:Young/Full GC时间
    

  3. 隔离

    • 从负载均衡摘掉问题实例

    • 保留现场,不要重启

  4. Dump

    # 推荐方式(JDK 9+)
    jcmd <pid> GC.heap_dump /data/logs/heap.hprof
    
    # 或jmap(老版本)
    jmap -dump:format=b,file=heap.hprof <pid>
    
    # 注意:dump会STW,大堆可能需要几十秒
    

  5. 分析

    # MAT分析
    # 1. 打开heap.hprof
    # 2. 选择 "Leak Suspects Report"
    # 3. 查看可疑对象
    

  6. 定位

    • 看GC Roots引用链

    • 找到持有对象的代码位置

    • 确认是代码bug还是设计问题

  7. 修复

    • 修复代码

    • 本地压测验证

    • 灰度发布

  8. 验证

    • 监控内存趋势

    • 观察GC频率

    • 运行72小时无问题=修复成功

  9. 预防

    • 代码Review

    • 压测覆盖

    • 监控告警

5.3 常见内存泄漏场景

场景1:ThreadLocal未清理

问题代码

public class UserService {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
    
    public void handleRequest(User user) {
        currentUser.set(user);
        // 业务逻辑
    }
    // 忘记调用 currentUser.remove()
}

为什么泄漏

  • 线程池复用线程,ThreadLocal值不会自动清理

  • 每次请求都set,值越来越多

  • 最终OOM

修复

public void handleRequest(User user) {
    try {
        currentUser.set(user);
        // 业务逻辑
    } finally {
        currentUser.remove();  // 必须清理
    }
}

场景2:静态集合只增不减

问题代码

public class CacheManager {
    private static final Map<String, Object> cache = new HashMap<>();
    
    public void addCache(String key, Object value) {
        cache.put(key, value);
    }
    // 没有删除逻辑
}

修复

// 方案1:使用弱引用
private static final Map<String, WeakReference<Object>> cache = new WeakHashMap<>();

// 方案2:使用有界缓存(推荐)
private static final Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(30))
    .build();

场景3:监听器未注销

问题代码

public class EventListener {
    public void register() {
        EventBus.register(this);  // 注册监听器
    }
    // 对象销毁时没有unregister
}

修复

public void destroy() {
    EventBus.unregister(this);  // 必须注销
}

场景4:ClassLoader泄漏

场景:热部署、动态加载类时,旧的ClassLoader没有被回收

排查

  • 看元空间是否持续增长

  • MAT分析ClassLoader引用链

修复

  • 确保自定义ClassLoader实现正确

  • 热部署时显式调用 close() 方法

5.4 实战案例

场景:支付系统,运行3天后OOM

排查过程

  1. jstat监控

    jstat -gcutil 12345 1000
    

    发现:老年代每小时增长200MB,Full GC后回收不到5%

  2. Dump堆

    jcmd 12345 GC.heap_dump /tmp/heap.hprof
    

  3. MAT分析

    • Leak Suspects报告指出:byte[] 占用60%堆内存

    • 追溯引用链:byte[]StringHashMapRequestContext

  4. 定位代码

    public class RequestContext {
        private static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();
        
        public static void set(String key, String value) {
            Map<String, String> map = context.get();
            if (map == null) {
                map = new HashMap<>();
                context.set(map);
            }
            map.put(key, value);
        }
        // 问题:没有remove,ThreadLocal中的Map越来越大
    }
    

  5. 根因

    • 请求上下文用ThreadLocal存储,但没清理

    • 线程池复用,Map只增不减

    • 3天累积,内存爆了

  6. 修复

    public class RequestContext {
        public static void clear() {
            context.remove();  // 请求结束后必须调用
        }
    }
    
    // 在Filter中统一清理
    public class RequestFilter implements Filter {
        public void doFilter(...) {
            try {
                chain.doFilter(request, response);
            } finally {
                RequestContext.clear();
            }
        }
    }
    

  7. 验证

    • 压测24小时

    • 监控老年代稳定在30%,无增长

    • 上线后运行7天无问题

面试怎么说

"我们支付系统之前出过OOM,运行3天就挂。通过jstat监控发现老年代持续增长,Full GC回收不了。Dump后用MAT分析,发现是ThreadLocal泄漏。请求上下文存在ThreadLocal里,但没清理,线程池复用导致Map越来越大。修复方案是在Filter里统一调用remove(),同时加了监控告警,老年代超过60%就报警。"


6. CPU 飙高排查

6.1 完整排查步骤

# Step 1: 找到CPU高的进程
top -c
# 看 %CPU 列,找到最高的进程PID

# Step 2: 找到该进程下CPU高的线程
top -Hp <pid>
# 看 %CPU 列,找到最高的线程TID

# Step 3: 线程ID转16进制
printf "%x\n" <tid>
# 输出:1a2b(这就是jstack里的nid)

# Step 4: 导出线程栈
jstack <pid> > thread_dump.txt

# Step 5: 在thread_dump.txt中搜索nid
grep -A 30 "nid=0x1a2b" thread_dump.txt
# 找到对应的Java代码位置

6.2 Arthas实战命令

# 启动Arthas
java -jar arthas-boot.jar

# 查看整体情况
dashboard
# 看线程CPU占用、内存、GC情况

# 查看线程详情
thread -n 3
# 输出CPU最高的3个线程

# 查看线程栈
thread <tid>
# 输出指定线程的调用栈

# 追踪方法调用耗时
trace com.example.OrderService createOrder
# 看每个子方法耗时,定位慢点

# 监控方法入参和返回值
watch com.example.OrderService createOrder '{params, returnObj}' -x 2
# -x 2 表示展开2层

# 反编译代码
jad com.example.OrderService
# 看运行时真实的代码,防止代码和class不一致

# 查看类加载信息
sc com.example.OrderService
# 看类是从哪个jar加载的

6.3 常见原因及解决方案

原因1:死循环

现象:某个线程CPU 100%,持续不降

排查

jstack <pid> | grep -A 30 "nid=0x1a2b"

看到:

"thread-name" #xx daemon prio=5
   java.lang.Thread.State: RUNNABLE
    at com.example.BugService.process(BugService.java:42)

定位代码

// 问题代码
while (true) {
    // 没有break条件
    doSomething();
}

修复:加退出条件或超时机制

原因2:频繁GC

现象:多个线程CPU高,且都在GC相关方法

排查

jstat -gcutil <pid> 1000
# 看FGC(Full GC次数)和FGCT(Full GC时间)

发现:FGC频繁,每秒1-2次

根因:内存不足,对象创建太快

解决

  • 排查内存泄漏(见第5章)

  • 调大堆内存

  • 优化代码减少对象创建

原因3:锁竞争

现象:大量线程BLOCKED,CPU高但吞吐量低

排查

jstack <pid> | grep -B 2 "BLOCKED"
# 看哪些线程在等锁

或用Arthas:

thread -b
# 查看阻塞其他线程的锁

根因:锁粒度太大或锁持有时间太长

解决

  • 减小锁粒度(分段锁)

  • 用读写锁替代互斥锁

  • 优化代码减少锁持有时间

6.4 实战案例

场景:秒杀系统,活动开始后CPU飙到90%

排查过程

  1. top看进程

    top -c
    

    Java进程CPU 90%

  2. top看线程

    top -Hp 12345
    

    发现3个线程CPU都在30%

  3. jstack分析

    jstack 12345 > thread.txt
    grep -A 30 "nid=0x3a4b" thread.txt
    

    看到:
    "pool-1-thread-3" #xx daemon prio=5
       java.lang.Thread.State: RUNNABLE
        at java.util.HashMap.get(HashMap.java:xxx)
        at com.example.SeckillService.getDeduction(SeckillService.java:89)
        at com.example.SeckillService.seckill(SeckillService.java:45)
    

  4. 定位代码

    public class SeckillService {
        private Map<String, Integer> deduction = new HashMap<>();  // 问题:非线程安全
        
        public int getDeduction(String key) {
            return deduction.get(key);
        }
        
        public void seckill() {
            // 并发读写HashMap,导致死循环
            int deduct = getDeduction(key);
            // ...
        }
    }
    

  5. 根因

    • HashMap在并发读写时可能死循环(JDK 7)或数据丢失(JDK 8+)

    • 秒杀场景并发量高,触发了问题

  6. 修复

    // 方案1:改用ConcurrentHashMap
    private Map<String, Integer> deduction = new ConcurrentHashMap<>();
    
    // 方案2:用Caffeine缓存(推荐)
    private Cache<String, Integer> deduction = Caffeine.newBuilder()
        .maximumSize(10_000)
        .build();
    

  7. 验证

    • 压测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生成堆dumpheapdump /tmp/heap.hprof
gc查看GC信息看各代内存使用情况

7.3 Class/ClassLoader相关

命令用途使用场景
sc搜索类sc *OrderService 查找所有OrderService
sm搜索方法sm com.example.OrderService * 看所有方法
jad反编译jad com.example.OrderService 看运行时代码
dumpdump类字节码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:线上接口慢,定位耗时

# 1. 看整体情况
dashboard

# 2. 追踪方法调用链
trace com.example.OrderService createOrder
# 输出:
# [0.5ms] com.example.OrderService:validate()
# [150ms] com.example.OrderService:saveToDB()  # 这里慢
# [2ms] com.example.OrderService:sendMQ()

场景2:查看方法入参和返回值

watch com.example.OrderService createOrder '{params, returnObj}' -x 2
# 输出:
# result: OrderDTO(id=123, status=PAID)
# params: CreateOrderRequest(userId=456, productId=789)

场景3:查看方法被谁调用

stack com.example.OrderService createOrder
# 输出:
# ts=xxx;thread_name=http-nio-8080-exec-1;
#     @com.example.OrderController.create()
#     @com.example.OrderService.createOrder()

场景4:线上代码和Git不一致,确认实际代码

jad com.example.OrderService
# 反编译看实际运行的代码
# 确认是不是部署的版本有问题

场景5:紧急修复,热更新代码

# 1. 本地修改代码,编译
mc /tmp/OrderService.java -d /tmp

# 2. 热更新
redefine /tmp/com/example/OrderService.class

# 注意:只能修改方法体,不能增减方法/字段

场景6:记录方法调用,事后分析

# 1. 开始记录
tt -t com.example.OrderService createOrder

# 2. 调用几次后,查看记录
tt -l
# 输出:
# INDEX  TIMESTAMP         COST(ms)  IS-RETURN  IS-EXP  OBJECT
# 1000   2026-08-10 10:00  150       true       false   0x1234
# 1001   2026-08-10 10:01  200       false      true    0x1234

# 3. 查看某次调用的详情
tt -i 1001 -p
# 输出:
# result: Exception: NullPointerException
# params: CreateOrderRequest(userId=null, productId=789)  # 发现问题

面试怎么说

"Arthas是线上排查神器,我最常用的几个命令:dashboard看整体情况,thread定位CPU高的线程,trace追踪方法耗时定位慢点,watch看入参出参排查数据问题,stack看调用链路,jad反编译确认代码版本。之前有个线上bug,用tt命令记录了几次调用,发现传入的userId是null,快速定位到问题。"


8. JDK 8 vs JDK 17 核心差异与面试考点

8.1 GC 选型差异

维度JDK 8JDK 17
默认 GCParallel(吞吐量优先)G1(延迟优先)
可选 GCCMS、G1ZGC、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 8JDK 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 8JDK 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 8JDK 17
反射访问宽松(默认可访问)严格(强封装)
--add-opens不需要反射访问内部 API 必须加
Security Manager可用Deprecated(未来移除)
TLS 版本TLS 1.0/1.1 仍支持默认只支持 TLS 1.2+

JDK 17 反射限制

# 常见框架需要加的参数(Spring、Hibernate 等)
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED

面试怎么说

"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问题,本质是想看:

  1. 你懂不懂原理(内存模型、GC算法)

  2. 你分不分得清版本(JDK 8 vs 17 的 GC、日志、参数差异)

  3. 你用过什么工具(jstat、jmap、Arthas、MAT)

  4. 你解决过什么问题(实战案例)

  5. 你能不能预防问题(监控、规范)

JDK 8 + JDK 17 双版本面试小贴士

  • 先把 JDK 8(默认 Parallel)和 JDK 17(默认 G1)的差异记牢,这是高频考点

  • 背熟 GC 日志在两套版本下的写法差异

  • 准备一个"JDK 8 升 17"的实战叙述(CMS 迁移、--add-opens、日志参数改造)

  • 实战案例要能讲清在哪个版本下发生的,用了什么工具

准备面试时,重点准备 3-5 个实战案例,每个都要能讲清楚:

  • 现象是什么

  • 怎么排查的

  • 根因是什么

  • 怎么解决的

  • 怎么预防的

参数可以忘,工具可以查文档,但排查思路和实战经验是你的核心竞争力。

祝面试顺利!

技术面试笔记 / 01_JVM调优实战手册_2026 0 0 cosolar
2026-08-30T02:14:22.217583785Z 2026-08-30T02:17:11.995223975Z