JVM 内存调优的方向

目录

[一、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 怎么排查?)

Q6:元空间和永久代有什么区别?

[Q7:STW 是什么?怎么减少?](#Q7:STW 是什么?怎么减少?)

[Q8:jstack 和 jmap 分别用在什么场景?](#Q8:jstack 和 jmap 分别用在什么场景?)

Q9:对象什么时候直接进老年代?

[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,不然死了都不知道怎么死的