Lesson 29 · JVM 原理与调优
JVM 调优参数手册:30 个最常用参数速查
面试官:你常用的 JVM 参数有哪些?
"就……-Xmx 设一下堆大小?" 大部分候选人的回答到此为止。如果你能系统地讲出堆内存、GC 收集器、GC 日志、诊断调试四大类共 30 个核心参数,并给出推荐值和使用场景——这就是一份满分的实战回答。
JVM 参数调优是 Java 面试中最"落地"的考点。它不像并发原理或集合源码那样有深度理论,但能直接反映你有没有真正上线过生产系统、有没有排查过线上故障。
本文将 30 个最常用的 JVM 参数按功能分为四大类:
| 类别 | 核心参数 | 解决的问题 |
|---|---|---|
| 堆内存参数 | -Xms, -Xmx, -Xmn, -Xss, -XX:MetaspaceSize 等 8 个 | 内存分配与布局 |
| GC 收集器参数 | -XX:+UseG1GC, -XX:+UseZGC, -XX:MaxGCPauseMillis 等 6 个 | 选择与调优收集器 |
| GC 日志参数 | -Xlog:gc*, -XX:+PrintGCDetails 等 6 个 | GC 行为可观测 |
| 诊断与调试参数 | -XX:+HeapDumpOnOutOfMemoryError 等 5 个 | 故障定位与分析 |
不要只是"背参数"。面试官真正想听到的是:你在什么场景下用了什么参数、为什么这么设、效果如何。本文每个参数都附带了推荐值和使用场景,帮你建立"参数 → 场景"的直觉。
堆内存参数:8 个必会参数
堆是 JVM 管理的最大内存区域,几乎所有对象实例都在这里分配。堆的大小和布局直接决定了 GC 的行为和性能表现。以下 8 个参数控制着堆的方方面面:
| 参数 | 含义 | 推荐值 | 说明 |
|---|---|---|---|
-Xms | 初始堆大小 | 与 -Xmx 相同 | 避免运行时堆扩容导致的 STW |
-Xmx | 最大堆大小 | 物理内存的 50%~75% | 留出系统 + 堆外内存余量 |
-Xmn | 新生代大小 | 堆的 1/3~1/2 | G1 下通常不手动设,交由 -XX:G1NewSizePercent 控制 |
-Xss | 每个线程栈大小 | 256k~512k | 默认 1MB,高并发场景适当减小以节省内存 |
-XX:NewRatio | 老年代/新生代比值 | 2(默认) | 设 2 表示老年代:新生代 = 2:1,即新生代占堆的 1/3 |
-XX:SurvivorRatio | Eden:Survivor 比值 | 8(默认) | 设 8 表示 Eden:From:To = 8:1:1 |
-XX:MetaspaceSize | Metaspace 初始阈值 | 256m~512m | 达到此值触发 Full GC 来卸载类,避免频繁 Full GC |
-XX:MaxMetaspaceSize | Metaspace 上限 | 512m~1g | 防止类加载泄漏无限占用系统内存 |
为什么推荐 -Xms 和 -Xmx 设为相同值?
如果初始堆小于最大堆,JVM 在运行过程中需要不断扩容。每次扩容都是一次 STW(Stop-The-World),会暂停所有应用线程。对于生产环境,这是完全可以避免的停顿。设成相同值,让 JVM 在启动时就申请好所有内存。
参数间的关系
MetaspaceSize 的陷阱
很多线上系统的频繁 Full GC 问题,罪魁祸首就是 -XX:MetaspaceSize 的默认值太小(JDK 8 默认约 21MB)。Metaspace 在类加载时增长,一旦达到当前阈值就会触发 Full GC 来尝试卸载无用类。如果初始阈值太低,应用启动后短时间内会触发多次 Full GC。
java \ -Xms4g -Xmx4g # 初始=最大,避免运行时扩容 -Xmn1536m # 新生代 1.5g(堆的 ~38%),Parallel GC 下手动指定 -Xss512k # 线程栈减半,高并发场景节省内存 -XX:MetaspaceSize=512m # 初始阈值提高,避免启动后频繁 Full GC -XX:MaxMetaspaceSize=512m # 设上限,防止类加载泄漏吃光系统内存 -jar app.jar
典型 4GB 堆的进程总内存 ≈ 4G(堆) + 0.5G(Meta) + 0.5G(1000线程×512k) + 0.3G(其他) ≈ 5.3GB
生产环境我会把 -Xms 和 -Xmx 设成一样大,避免运行时扩容带来的 STW。MetaspaceSize 设到 256m~512m,避免默认值太小导致启动后频繁 Full GC。线程栈 -Xss 在并发量大的服务里从默认 1MB 降到 512k,可以节省几百 MB 内存。G1 下一般不手动设 -Xmn,而是用 G1NewSizePercent 让 G1 自己调整新生代大小。
GC 收集器参数:如何选择合适的收集器
选择 GC 收集器是 JVM 调优中最关键的决策——它决定了你的应用在吞吐量、延迟、内存开销三者之间的取舍。以下是 6 个核心参数:
| 参数 | 收集器 | 算法 | 适用场景 |
|---|---|---|---|
-XX:+UseSerialGC | Serial | 单线程 STW | 小堆 (<100MB)、单核 CPU、容器内 |
-XX:+UseParallelGC | Parallel | 多线程 STW | 批量处理、后台任务、吞吐量优先 |
-XX:+UseG1GC | G1 | Region 化 + 混合回收 | 通用 Web 服务、中等堆 (4~64GB)、JDK 9+ 默认 |
-XX:+UseZGC | ZGC | 染色指针 + 读屏障 | 大堆 (>8GB)、超低延迟要求、JDK 15+ |
-XX:MaxGCPauseMillis | G1 / Parallel | 停顿目标 | G1 默认 200ms,延迟敏感可调到 50~100ms |
-XX:G1HeapRegionSize | G1 | Region 大小 | 1m~32m(2 的幂),目标 ~2048 个 Region |
为什么不直接全部用 ZGC?它停顿最低,不是最好的吗?
ZGC 的低停顿是以吞吐量和内存开销为代价的。染色指针和读屏障会带来 10%~15% 的吞吐量损失,ZGC 还需要额外的内存来维护颜色和转发表。对于堆小于 4GB、对延迟要求不极端的普通 Web 服务,G1 的性价比远高于 ZGC。ZGC 最适合的是大堆 + 低延迟的场景。
收集器选择决策树
# ① Serial GC —— 容器内小服务 java -XX:+UseSerialGC -Xms128m -Xmx128m -jar app.jar # ② Parallel GC —— 批量数据处理 java -XX:+UseParallelGC -Xms8g -Xmx8g \ -XX:ParallelGCThreads=8 -XX:MaxGCPauseMillis=500 -jar app.jar # ③ G1 GC —— 通用 Web 服务(推荐默认选择) java -XX:+UseG1GC -Xms4g -Xmx4g \ -XX:MaxGCPauseMillis=100 -XX:G1HeapRegionSize=4m -jar app.jar # ④ ZGC —— 大堆 + 超低延迟 java -XX:+UseZGC -Xms16g -Xmx16g \ -XX:+ZGenerational -jar app.jar # JDK 21+ 分代 ZGC
选 GC 收集器就是选"停顿 vs 吞吐量 vs 内存"的取舍策略。小堆单核用 Serial,吞吐量优先用 Parallel,通用场景用 G1(JDK 9+ 默认),大堆 + 超低延迟用 ZGC。G1 的 MaxGCPauseMillis 默认 200ms,延迟敏感的服务可以调低到 50~100ms,但设太低会导致吞吐量下降。JDK 21 的分代 ZGC 是目前延迟与吞吐量综合最优的选择。
GC 日志参数:JDK 8 vs JDK 9+ 两套体系
GC 日志是排查线上 GC 问题的第一手资料。但 JDK 8 和 JDK 9+ 的 GC 日志参数完全不同,这是面试中极易踩坑的考点。
| JDK 版本 | 参数 | 作用 |
|---|---|---|
| JDK 8 及以前 | -XX:+PrintGCDetails | 打印每次 GC 的详细信息 |
-XX:+PrintGCDateStamps | 在每行日志前加时间戳 | |
-Xloggc:/path/gc.log | GC 日志输出到文件 | |
-XX:+UseGCLogFileRotation | 启用日志文件轮转 | |
-XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M | 保留 5 个文件,每个 20MB | |
| JDK 9+(统一日志) | -Xlog:gc*:file=/path/gc.log:time,level,tags | 统一日志框架,gc* 匹配所有 GC 标签 |
-Xlog:gc*=info:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m | 完整配置:info 级别 + 轮转 | |
-Xlog:gc+heap=debug | 细化到 heap 子标签,debug 级别 |
JDK 9+ 的统一日志框架相比 JDK 8 的 PrintGCDetails 有什么优势?
JDK 8 的 GC 日志格式不统一——不同收集器输出的格式完全不同,解析困难。JDK 9+ 的 -Xlog 统一了所有子系统的日志格式(GC、类加载、JIT 编译等),用标签(tags)和级别(levels)来过滤,可以用标准工具解析。而且内置了日志轮转,不需要额外配置。
java \ -XX:+PrintGCDetails \ -XX:+PrintGCDateStamps \ -XX:+PrintGCTimeStamps \ -XX:+PrintTenuringDistribution \ -Xloggc:/data/logs/gc/gc.log \ -XX:+UseGCLogFileRotation \ -XX:NumberOfGCLogFiles=10 \ -XX:GCLogFileSize=20M \ -jar app.jar
java \ -Xlog:gc*,gc+age=trace,safepoint:file=/data/logs/gc/gc.log:\ time,uptime,level,tags:filecount=10,filesize=20m \ -jar app.jar # 日志输出样例 # [2024-01-15T10:30:00.123+0800][1.234s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 209M->42M(4096M) 8.234ms # 解析:时间戳 | JVM运行时间 | 级别 | 标签 | GC编号 | 类型 | 原因 | 回收前->回收后(堆总量) | 停顿时间
GC 日志关键字段速读
| 字段 | 示例 | 含义 |
|---|---|---|
| GC 类型 | Pause Young / Pause Full / Concurrent Mark | Young GC / Full GC / 并发标记 |
| 原因 | G1 Evacuation Pause / Allocation Failure | 触发 GC 的原因 |
| 内存变化 | 209M→42M(4096M) | 回收前→回收后(堆总量) |
| 停顿时间 | 8.234ms | STW 停顿的墙钟时间 |
JDK 8 用 -XX:+PrintGCDetails + -Xloggc 来输出 GC 日志,JDK 9+ 改用统一的 -Xlog:gc* 框架。生产环境一定要开 GC 日志并配置轮转——它是排查 GC 停顿、Full GC 频率、内存泄漏的第一手数据。我一般用 GCEasy 或 gcviewer 来分析日志,可以快速看到 GC 频率、停顿时间分布和内存趋势。
诊断与调试参数:线上排障的救命稻草
当线上出现 OOM、崩溃、性能退化时,以下参数能帮你自动保留现场,是生产环境必配的安全网:
| 参数 | 作用 | 推荐配置 |
|---|---|---|
-XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump 堆内存 | 生产必开 |
-XX:HeapDumpPath | 指定 dump 文件路径 | /data/dumps/heap.hprof |
-XX:ErrorFile | JVM 致命错误日志路径 | /data/logs/jvm/hs_err_%p.log |
-XX:+PrintFlagsFinal | 打印所有 JVM 参数的最终值 | 启动时验证参数生效 |
-XX:+UnlockDiagnosticVMOptions | 解锁隐藏的诊断参数 | 高级调优时使用 |
HeapDump 文件动辄几个 GB,怎么分析?
用 Eclipse MAT(Memory Analyzer Tool)打开 .hprof 文件,它会生成"Leak Suspects Report"(泄漏嫌疑报告),自动找出占用内存最大的对象和 GC Root 引用链。重点关注"Dominator Tree"——它显示哪些对象"支配"了最多的内存保留。
java \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dumps/heap_%p.hprof # %p = PID,避免覆盖 -XX:ErrorFile=/data/logs/jvm/hs_err_%p.log # JVM 崩溃日志 -XX:+PrintFlagsFinal # 启动时打印所有参数最终值 -jar app.jar # 验证参数是否生效 java -XX:+PrintFlagsFinal -version 2>&1 | grep HeapDump # 输出:bool HeapDumpOnOutOfMemoryError = true {product}
ErrorFile 的价值
当 JVM 因为段错误(SIGSEGV)、栈溢出(StackOverflowError 导致崩溃级别)等致命错误退出时,会在 ErrorFile 路径下生成 hs_err_pid<N>.log。这个文件包含了:
- 崩溃线程的堆栈信息——定位崩溃发生的代码位置
- JVM 参数和版本——确认运行时配置
- 堆使用概况——崩溃前的内存状态
- 加载的动态库列表——排查 JNI/native 问题
实用诊断命令(运行时)
# 查看 JVM 启动参数 jcmd <pid> VM.flags # 查看堆内存使用概况 jmap -heap <pid> # 触发一次堆 dump(不用等 OOM) jmap -dump:format=b,file=heap.hprof <pid> # 查看 GC 统计摘要 jstat -gcutil <pid> 1000 10 # 每 1 秒采样,共 10 次 # 查看线程堆栈 jstack <pid> > threads.txt
生产环境必开 -XX:+HeapDumpOnOutOfMemoryError,配合 HeapDumpPath 指定到磁盘空间充足的目录。这样 OOM 发生时 JVM 自动保存堆快照,事后用 MAT 分析就能定位内存泄漏的根因。ErrorFile 则是 JVM 崩溃时的"黑匣子",包含崩溃线程堆栈和内存状态。-XX:+PrintFlagsFinal 可以在启动时验证所有参数是否按预期生效,避免配置写错导致的线上问题。
生产就绪:三种场景的 JVM 启动模板
把前 5 站的参数组合起来,针对三种最常见的生产场景,给出可以直接复制使用的启动脚本:
场景一:通用 Web 服务(Spring Boot / 微服务)
java \ # 堆内存 -Xms4g -Xmx4g \ -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \ -Xss512k \ # GC 收集器 -XX:+UseG1GC \ -XX:MaxGCPauseMillis=100 \ -XX:G1HeapRegionSize=4m \ -XX:InitiatingHeapOccupancyPercent=45 \ # GC 日志(JDK 17+) -Xlog:gc*,gc+age=trace:file=/data/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m \ # 诊断 -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dumps/heap_%p.hprof \ -XX:ErrorFile=/data/logs/hs_err_%p.log \ -jar app.jar
场景二:批量处理 / 数据 ETL
java \ # 堆内存 -Xms8g -Xmx8g \ -Xmn4g \ -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \ # GC 收集器 -XX:+UseParallelGC \ -XX:ParallelGCThreads=8 \ -XX:MaxGCPauseMillis=500 \ # GC 日志 -Xlog:gc*:file=/data/logs/gc.log:time,level,tags:filecount=5,filesize=20m \ # 诊断 -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dumps/heap_%p.hprof \ -jar batch-app.jar
场景三:超低延迟微服务(金融 / 实时竞价)
java \ # 堆内存 -Xms16g -Xmx16g \ -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1g \ -Xss512k \ # GC 收集器 -XX:+UseZGC \ -XX:+ZGenerational \ -XX:SoftMaxHeapSize=14g \ # GC 日志 -Xlog:gc*,gc+heap=debug:file=/data/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=20m \ # 诊断 -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dumps/heap_%p.hprof \ -XX:ErrorFile=/data/logs/hs_err_%p.log \ # 性能优化 -XX:-UseBiasedLocking \ -XX:AutoBoxCacheMax=20000 \ -jar latency-sensitive.jar
以上模板需要根据实际业务负载调整。特别是 -Xmx 的大小,需要基于压测数据确定——观察 Full GC 后的存活对象大小,乘以 2~3 倍作为堆大小的参考值。不要"拍脑袋"设一个很大的堆,过大的堆会导致 GC 时间变长(Parallel / G1)或内存浪费(ZGC 的额外开销)。
全文速查清单
- 堆内存:-Xms=-Xmx 避免扩容 STW;MetaspaceSize 设 256m+ 避免启动 Full GC;-Xss 高并发时降到 512k
- GC 收集器:通用选 G1,吞吐选 Parallel,超低延迟选 ZGC;MaxGCPauseMillis 按 SLA 设置
- GC 日志:JDK 8 用 PrintGCDetails,JDK 9+ 用 -Xlog:gc*;必须配轮转
- 诊断:HeapDumpOnOutOfMemoryError 必开;ErrorFile 指定路径;PrintFlagsFinal 验证配置
- 核心原则:所有参数都需要基于压测数据调优,不能照搬模板
JVM 参数调优的核心不是"背参数",而是理解每个参数在"停顿-吞吐-内存"三角中的位置。面试时如果能按"堆内存 → GC 收集器 → GC 日志 → 诊断保护"这四个维度系统回答,并给出推荐值和选型理由,就是一份有经验、有深度的满分回答。