目录
[一、JVM 调优是什么](#一、JVM 调优是什么)
[二、JVM 内存结构(调优的前提)](#二、JVM 内存结构(调优的前提))
[2.1 堆(Heap)--- 调优的主战场](#2.1 堆(Heap)— 调优的主战场)
[2.2 栈(Stack)--- 线程私有](#2.2 栈(Stack)— 线程私有)
[2.3 方法区(元空间)](#2.3 方法区(元空间))
[三、GC 垃圾回收机制](#三、GC 垃圾回收机制)
[3.1 怎么判断对象是垃圾](#3.1 怎么判断对象是垃圾)
[3.2 垃圾回收算法](#3.2 垃圾回收算法)
[3.3 常见的垃圾回收器](#3.3 常见的垃圾回收器)
[四、JVM 参数分类](#四、JVM 参数分类)
[4.1 堆内存参数](#4.1 堆内存参数)
[4.2 GC 相关参数](#4.2 GC 相关参数)
[4.3 栈和线程参数](#4.3 栈和线程参数)
[4.4 OOM 相关参数](#4.4 OOM 相关参数)
[五、JVM 调优实战流程](#五、JVM 调优实战流程)
[第二步:分析 GC 日志关键指标](#第二步:分析 GC 日志关键指标)
[场景 1:Young GC 太频繁](#场景 1:Young GC 太频繁)
[场景 2:Full GC 频繁](#场景 2:Full GC 频繁)
[场景 3:GC 停顿时间太长](#场景 3:GC 停顿时间太长)
[场景 4:元空间 OOM](#场景 4:元空间 OOM)
[场景 5:栈溢出](#场景 5:栈溢出)
[场景 6:OOM 了但不知道哪里](#场景 6:OOM 了但不知道哪里)
[六、典型应用场景的 JVM 参数模板](#六、典型应用场景的 JVM 参数模板)
[6.1 微服务(2C4G)](#6.1 微服务(2C4G))
[6.2 大内存服务(8C16G)](#6.2 大内存服务(8C16G))
[6.3 低延迟场景(ZGC,JDK 17+)](#6.3 低延迟场景(ZGC,JDK 17+))
[Q1:JVM 调优从哪开始?](#Q1:JVM 调优从哪开始?)
[Q2:Full GC 频繁怎么办?](#Q2:Full GC 频繁怎么办?)
[Q3:怎么判断用 Parallel 还是 G1?](#Q3:怎么判断用 Parallel 还是 G1?)
[Q4:-Xms 和 -Xmx 为什么要设一样?](#Q4:-Xms 和 -Xmx 为什么要设一样?)
[Q5:OOM 怎么排查?](#Q5:OOM 怎么排查?)
[Q7:STW 是什么?怎么减少?](#Q7:STW 是什么?怎么减少?)
[Q8:jstack 和 jmap 分别用在什么场景?](#Q8:jstack 和 jmap 分别用在什么场景?)
[Q10:JVM 调优有哪些常用参数组合?](#Q10:JVM 调优有哪些常用参数组合?)
一、JVM 调优是什么
JVM 调优就是通过调整 JVM 参数,让 Java 应用运行得更快、更稳、不 OOM。
不是炫技,而是解决实际问题:
| 问题 | 调什么 |
|---|---|
| 接口响应慢,频繁 GC | 垃圾回收器选型 + GC 参数 |
| 系统突然 OOM 挂了 | 堆内存大小 + 内存泄漏排查 |
| CPU 飙高 | GC 线程 / 死循环代码 |
| 吞吐量上不去 | 堆比例、并发线程数 |
JVM 调优 = 先有监控数据 → 分析瓶颈 → 改参数 → 验证效果。没有监控就调优是耍流氓。
二、JVM 内存结构(调优的前提)
你调的都是下面这块地的尺寸:
另外:栈(线程私有)、程序计数器、本地方法栈
2.1 堆(Heap)--- 调优的主战场
存放对象实例,所有线程共享。分三块:
| 区域 | 比例(默认) | 特点 |
|---|---|---|
| 新生代(Young) | 堆的 1/3 | 新对象在这,GC 频繁 |
| └ Eden | 新生代的 8/10 | 对象首先分配在这 |
| └ Survivor From | 新生代的 1/10 | GC 后存活的对象移到这 |
| └ Survivor To | 新生代的 1/10 | 复制算法用,始终是空的 |
| 老年代(Old) | 堆的 2/3 | 长期存活的对象在这,GC 少 |
| 元空间(MetaSpace) | 默认无上限 | 类信息、常量池,不归堆管 |
2.2 栈(Stack)--- 线程私有
每个线程一个栈,存局部变量、方法调用链。栈深度不够就 StackOverflowError。
2.3 方法区(元空间)
存类信息、常量、静态变量。Java 8 之前叫永久代(PermGen),之后改元空间(MetaSpace),从堆移到了本地内存。
为什么改? 永久代大小固定,类多了容易 OOM(PermGen space)。元空间用本地内存,理论只受物理内存限制。
三、GC 垃圾回收机制
3.1 怎么判断对象是垃圾
可达性分析算法(面试必问):
GC Roots 包括:栈帧中的局部变量、静态变量、JNI 引用。
3.2 垃圾回收算法
| 算法 | 原理 | 适用 |
|---|---|---|
| 标记-清除 | 标记垃圾 → 清除 | 老年代(CMS 用) |
| 标记-复制 | 分两块,用一块存活的复制到另一块 | 新生代(默认) |
| 标记-整理 | 标记存活 → 向一端移动 → 清理边界外 | 老年代 |
3.3 常见的垃圾回收器
| 回收器 | 适用代 | 特点 | 常用参数 |
|---|---|---|---|
| Serial | 新生代 | 单线程,STW 长 | 客户端默认 |
| ParNew | 新生代 | Serial 的多线程版 | CMS 的搭档 |
| Parallel Scavenge | 新生代 | 关注吞吐量 | 服务端默认(JDK 8) |
| Parallel Old | 老年代 | 吞吐量优先 | 跟 Parallel Scavenge 配对 |
| CMS | 老年代 | 低延迟,并发 | JDK 9 起废弃 |
| G1 | 全堆 | 分区回收,可预测停顿 | JDK 9+ 默认 |
| ZGC | 全堆 | 极低延迟(<10ms) | JDK 11+ 实验性,JDK 17+ 可用 |
| Shenandoah | 全堆 | 同 ZGC,Red Hat 出品 | JDK 12+ |
一句话选型:
JDK 8 默认:Parallel Scavenge + Parallel Old(吞吐量优先)
JDK 9+ 默认:G1(平衡延迟和吞吐量)
追求低延迟(<10ms):ZGC(大堆大内存)
小应用(<4G 堆):Parallel 够用
四、JVM 参数分类
4.1 堆内存参数
# 堆大小 -Xms4g # 初始堆大小(启动时就分配) -Xmx4g # 最大堆大小(通常 = Xms,避免动态扩缩) -Xmn2g # 新生代大小 # 各代比例 -XX:NewRatio=2 # 老年代:新生代 = 2:1,即新生代占堆 1/3 -XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1 # 元空间 -XX:MetaspaceSize=256m # 元空间初始大小 -XX:MaxMetaspaceSize=256m # 元空间最大(不设就是物理内存上限) # 堆溢出时 Dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
4.2 GC 相关参数
# 选垃圾回收器 -XX:+UseG1GC # 用 G1 -XX:+UseParallelGC # 用 Parallel -XX:+UseConcMarkSweepGC # 用 CMS(JDK 9 废弃) -XX:+UseZGC # 用 ZGC(JDK 17+) # GC 日志(JDK 8) -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log # GC 日志(JDK 9+,统一格式) -Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags # G1 专用 -XX:MaxGCPauseMillis=200 # 期望最大停顿时间(ms) -XX:G1HeapRegionSize=2m # G1 分区大小 -XX:InitiatingHeapOccupancyPercent=45 # 触发并发 GC 的堆占比
4.3 栈和线程参数
-Xss256k # 每个线程的栈大小(默认 1M,减到 256k-512k 能开更多线程) -XX:ThreadStackSize=256 # 同上,单位 k
4.4 OOM 相关参数
-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动 Dump -XX:HeapDumpPath=/data/dump/ # Dump 文件保存路径 -XX:OnOutOfMemoryError="kill -9 %p" # OOM 时执行脚本(比如重启)
五、JVM 调优实战流程
第一步:拿到监控数据(没数据别调)
# 1. 查看 JVM 进程 jps -l # 2. 查看堆使用情况 jstat -gc <pid> 1000 10 # 每 1 秒打印一次,共 10 次 # 3. 查看 GC 情况 jstat -gcutil <pid> 1000 # 4. 线程堆栈分析 jstack <pid> # 5. 堆 Dump 分析 jmap -dump:live,format=b,file=heap.hprof <pid> # 6. 图形化分析工具 # jvisualvm(JDK 8 自带) # JProfiler / MAT(第三方)
第二步:分析 GC 日志关键指标
拿到 GC 日志后看什么:
# 一段 GC 日志(Parallel 示例) 2026-06-24T10:00:00.123+0800: [GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] 2048K->1024K(7680K), 0.0012345 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
| 指标 | 正常 | 警报 |
|---|---|---|
| YGC 频率 | 几秒到几十秒一次 | 一秒多次 |
| YGC 耗时 | < 50ms | > 200ms |
| Full GC 频率 | 几小时甚至没有 | 几分钟一次 |
| Full GC 耗时 | < 200ms | > 1s |
| 堆使用率 | GC 后 20-40% | GC 后还在 80%+ |
| 吞吐量(用户时间/总时间) | > 99% | < 95% |
第三步:根据问题调参
场景 1:Young GC 太频繁
# 现象:GC 日志显示 YGC 一秒多次 # 原因:新生代太小,对象很快就满了 # 调大新生代 -Xmn2g # 从原来的 1G 调到 2G -XX:SurvivorRatio=8 # 保持 Eden:S0:S1 = 8:1:1
场景 2:Full GC 频繁
# 现象:频繁 Full GC,每次耗时几百毫秒甚至秒级 # 原因:老年代满了,多为大对象直接进老年代,或内存泄漏 # 首先排查内存泄漏: jmap -dump:live,format=b,file=heap.hprof <pid> # 用 MAT 分析大对象 # 如果确认不是泄漏: -XX:PretenureSizeThreshold=10m # 大于 10M 的对象才直接进老年代 -Xms4g -Xmx4g # 加大堆 -XX:+UseG1GC # 切 G1 减少 Full GC
场景 3:GC 停顿时间太长
# 现象:用户反映请求超时,GC 停顿超过 1 秒 # 方案 A:切 G1 控停顿 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 # 目标停顿 200ms # 方案 B:大内存上 ZGC(JDK 17+) -XX:+UseZGC -Xms16g -Xmx16g
场景 4:元空间 OOM
# 现象:java.lang.OutOfMemoryError: Metaspace # 主要是 CGLib 动态代理/反射生成太多类 -XX:MaxMetaspaceSize=256m # 限制元空间,快速暴露问题 # 然后排查哪个框架生成了大量类
场景 5:栈溢出
# 现象:StackOverflowError # 原因:递归太深 # 应急方案(治标不治本) -Xss512k # 加大栈,但治不了无限递归 # 根本方案:修代码,把递归改成循环
场景 6:OOM 了但不知道哪里
如果 OOM 了却不知道哪块内存满了,加 Dump 参数:
# 等下次 OOM 自动 dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/ # 然后用 MAT 分析 hprof 文件 # 重点看:大对象、GC Roots 路径、线程栈
六、典型应用场景的 JVM 参数模板
6.1 微服务(2C4G)
java -Xms2g -Xmx2g \ -Xmn1g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:MetaspaceSize=128m \ -XX:MaxMetaspaceSize=128m \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dump/ \ -Xlog:gc*:file=/data/logs/gc.log:time,uptime:filecount=5,filesize=10m \ -jar app.jar
6.2 大内存服务(8C16G)
java -Xms12g -Xmx12g \ -Xmn6g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=100 \ -XX:ParallelGCThreads=8 \ -XX:ConcGCThreads=4 \ -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=256m \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dump/ \ -Xlog:gc*:file=/data/logs/gc.log:time,uptime:filecount=5,filesize=20m \ -jar app.jar
6.3 低延迟场景(ZGC,JDK 17+)
java -Xms8g -Xmx8g \ -XX:+UseZGC \ -XX:ConcGCThreads=4 \ -XX:ParallelGCThreads=8 \ -Xlog:gc*:file=/data/logs/gc.log:time,uptime \ -jar app.jar
七、面试话术汇总
Q1:JVM 调优从哪开始?
话术:先做监控再调优。第一步是加 GC 日志参数,用 jstat 看 GC 频率和耗时,jmap 看堆使用情况。然后分析关键指标:YGC 频率、Full GC 频率、单次停顿时间、堆使用率曲线。不先看监控就拍参数,相当于蒙眼开车。
Q2:Full GC 频繁怎么办?
话术:分两步排查。第一步排除内存泄漏------用 jmap dump 堆用 MAT 分析大对象、GC Roots 路径。第二步如果不是泄漏,可以:① 调大堆内存;② 调大新生代比例让更多对象在 YGC 回收;③ 切 G1 减少 Full GC。Full GC 频繁往往是老年代满了,要么对象太多,要么大对象直接进老年代了。
Q3:怎么判断用 Parallel 还是 G1?
话术:堆 < 4G 且对停顿不敏感 → Parallel,吞吐量最高。堆 > 4G 或需要控制停顿时间 → G1,它能设置 MaxGCPauseMillis 控制每次 GC 的停顿上限。G1 把堆分成多个 Region,每次只回收一部分 Region,避免一次 STW 扫全堆。
Q4:-Xms 和 -Xmx 为什么要设一样?
话术:为了避免 JVM 运行时动态缩扩容。如果 Xms=1g, Xmx=4g,JVM 启动只占 1g,后面需要更多堆时会向操作系统申请,这个过程比较耗性能。两个值设成一样,启动时直接拿满,运行稳定。
Q5:OOM 怎么排查?
话术:加
-XX:+HeapDumpOnOutOfMemoryError参数,OOM 时自动 dump 堆文件。然后用 MAT 或 JProfiler 分析:第一步看大对象怀疑列表;第二步看 GC Roots 路径找到谁引用着它;第三步看线程栈,找到是在哪个业务代码里创建的。最常见的情况是:一个不断增长的集合(List/Map)、忘记关闭的流或连接、ThreadLocal 没用 remove。
Q6:元空间和永久代有什么区别?
话术:永久代是 JDK 7 及之前的方案,在堆内存里划了一块固定大小,类多了容易 OOM。JDK 8 改成了元空间,用的是本地内存(堆外),默认只受物理内存限制,不会轻易 OOM。改的原因就是永久代大小难估,换元空间更灵活。
Q7:STW 是什么?怎么减少?
话术:STW(Stop The World)指 GC 时所有业务线程暂停。减少 STW 的方式:① 用 G1/ZGC 这种并发回收器,大部分阶段跟业务线程一起跑;② 调 MaxGCPauseMillis 控制 G1 的停顿目标;③ 合理设置堆大小,避免 GC 太久;④ 减少 Full GC 次数,YGC 通常几十毫秒,Full GC 可能秒级。
Q8:jstack 和 jmap 分别用在什么场景?
话术:jstack 看线程,哪个线程卡住了、在等什么锁、是不是死锁了。jmap 看堆,堆占用、各代大小、还能 dump 堆文件。通常场景:CPU 飙高 → jstack 找线程,看是不是 GC 线程在狂跑;OOM → jmap dump 分析堆。
Q9:对象什么时候直接进老年代?
话术:三种情况:① 大对象(超过
-XX:PretenureSizeThreshold);② 动态年龄判定(Survivor 区放不下);③ 长期存活的对象(超过-XX:MaxTenuringThreshold次 YGC 还没回收)。如果发现大量对象直接进老年代,检查是不是大对象阈值设得偏低。
Q10:JVM 调优有哪些常用参数组合?
话术:常用组合是三件套:① 堆大小(-Xms -Xmx -Xmn);② 垃圾回收器(-XX:+UseG1GC / UseParallelGC / UseZGC);③ GC 日志(-Xlog:gc*)。实际项目中的模板通常是:堆设物理内存的 60-70%,保留一部分给元空间和系统,G1 设目标停顿 200ms。先跑一两天看 GC 日志再微调。
八、一句话速记
调优先看监控,没数据不改参数
GC 要控频率和停顿:
YGC 几秒一次、几十毫秒 → 正常
Full GC 频繁 → 排查泄漏或调参
微服务标配:G1 + 堆的 60% + GC 日志
OOM 必加 HeapDump,不然死了都不知道怎么死的