Java 面试笔记III · 06 / 14

Lesson 29 · JVM 原理与调优

JVM 调优参数手册:30 个最常用参数速查

中级·#JVM·#调优·#手册

第 1 站

面试官:你常用的 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 个故障定位与分析
面试加分项

不要只是"背参数"。面试官真正想听到的是:你在什么场景下用了什么参数、为什么这么设、效果如何。本文每个参数都附带了推荐值和使用场景,帮你建立"参数 → 场景"的直觉。

第 2 站

堆内存参数:8 个必会参数

堆是 JVM 管理的最大内存区域,几乎所有对象实例都在这里分配。堆的大小和布局直接决定了 GC 的行为和性能表现。以下 8 个参数控制着堆的方方面面:

参数含义推荐值说明
-Xms初始堆大小与 -Xmx 相同避免运行时堆扩容导致的 STW
-Xmx最大堆大小物理内存的 50%~75%留出系统 + 堆外内存余量
-Xmn新生代大小堆的 1/3~1/2G1 下通常不手动设,交由 -XX:G1NewSizePercent 控制
-Xss每个线程栈大小256k~512k默认 1MB,高并发场景适当减小以节省内存
-XX:NewRatio老年代/新生代比值2(默认)设 2 表示老年代:新生代 = 2:1,即新生代占堆的 1/3
-XX:SurvivorRatioEden:Survivor 比值8(默认)设 8 表示 Eden:From:To = 8:1:1
-XX:MetaspaceSizeMetaspace 初始阈值256m~512m达到此值触发 Full GC 来卸载类,避免频繁 Full GC
-XX:MaxMetaspaceSizeMetaspace 上限512m~1g防止类加载泄漏无限占用系统内存

为什么推荐 -Xms 和 -Xmx 设为相同值?

如果初始堆小于最大堆,JVM 在运行过程中需要不断扩容。每次扩容都是一次 STW(Stop-The-World),会暂停所有应用线程。对于生产环境,这是完全可以避免的停顿。设成相同值,让 JVM 在启动时就申请好所有内存。

参数间的关系

堆内存布局与参数控制 -Xmx / -Xms(总堆大小) 新生代 (-Xmn) Eden (8份) From To 老年代 SurvivorRatio=8 → Eden:From:To=8:1:1 NewRatio=2 → 老年代:新生代 = 2:1 Metaspace(-XX:MetaspaceSize / MaxMetaspaceSize) 存储类元数据,不在堆内(本地内存) 线程栈(-Xss × 线程数) 每个线程独占,默认 1MB,1000 线程 = 1GB
图 1 堆内存布局——各参数控制的区域一目了然

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
堆外内存估算 = Metaspace + 线程栈 × 线程数 + Direct Buffer + Code Cache + JVM 自身开销
典型 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 自己调整新生代大小。

第 3 站

GC 收集器参数:如何选择合适的收集器

选择 GC 收集器是 JVM 调优中最关键的决策——它决定了你的应用在吞吐量、延迟、内存开销三者之间的取舍。以下是 6 个核心参数:

参数收集器算法适用场景
-XX:+UseSerialGCSerial单线程 STW小堆 (<100MB)、单核 CPU、容器内
-XX:+UseParallelGCParallel多线程 STW批量处理、后台任务、吞吐量优先
-XX:+UseG1GCG1Region 化 + 混合回收通用 Web 服务、中等堆 (4~64GB)、JDK 9+ 默认
-XX:+UseZGCZGC染色指针 + 读屏障大堆 (>8GB)、超低延迟要求、JDK 15+
-XX:MaxGCPauseMillisG1 / Parallel停顿目标G1 默认 200ms,延迟敏感可调到 50~100ms
-XX:G1HeapRegionSizeG1Region 大小1m~32m(2 的幂),目标 ~2048 个 Region

为什么不直接全部用 ZGC?它停顿最低,不是最好的吗?

ZGC 的低停顿是以吞吐量和内存开销为代价的。染色指针和读屏障会带来 10%~15% 的吞吐量损失,ZGC 还需要额外的内存来维护颜色和转发表。对于堆小于 4GB、对延迟要求不极端的普通 Web 服务,G1 的性价比远高于 ZGC。ZGC 最适合的是大堆 + 低延迟的场景。

收集器选择决策树

GC 收集器选择决策 你的应用是什么类型? 堆<100MB / 单核 Serial GC 简单即最优 吞吐量优先 Parallel GC 批量 / 后台任务 通用 Web 服务 G1 GC JDK 9+ 默认 大堆 + 超低延迟 ZGC JDK 15+ 生产可用 G1 核心调优参数: -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:InitiatingHeapOccupancyPercent=45
图 2 GC 收集器选择决策——按堆大小、延迟需求、应用类型逐步缩小范围
各收集器启动参数对比
# ① 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 是目前延迟与吞吐量综合最优的选择。

第 4 站

GC 日志参数:JDK 8 vs JDK 9+ 两套体系

GC 日志是排查线上 GC 问题的第一手资料。但 JDK 8 和 JDK 9+ 的 GC 日志参数完全不同,这是面试中极易踩坑的考点。

JDK 版本参数作用
JDK 8 及以前 -XX:+PrintGCDetails打印每次 GC 的详细信息
-XX:+PrintGCDateStamps在每行日志前加时间戳
-Xloggc:/path/gc.logGC 日志输出到文件
-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)来过滤,可以用标准工具解析。而且内置了日志轮转,不需要额外配置。

JDK 8 生产环境 GC 日志配置
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
JDK 17+ 生产环境 GC 日志配置
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 MarkYoung GC / Full GC / 并发标记
原因G1 Evacuation Pause / Allocation Failure触发 GC 的原因
内存变化209M→42M(4096M)回收前→回收后(堆总量)
停顿时间8.234msSTW 停顿的墙钟时间
面试话术

JDK 8 用 -XX:+PrintGCDetails + -Xloggc 来输出 GC 日志,JDK 9+ 改用统一的 -Xlog:gc* 框架。生产环境一定要开 GC 日志并配置轮转——它是排查 GC 停顿、Full GC 频率、内存泄漏的第一手数据。我一般用 GCEasy 或 gcviewer 来分析日志,可以快速看到 GC 频率、停顿时间分布和内存趋势。

第 5 站

诊断与调试参数:线上排障的救命稻草

当线上出现 OOM、崩溃、性能退化时,以下参数能帮你自动保留现场,是生产环境必配的安全网:

参数作用推荐配置
-XX:+HeapDumpOnOutOfMemoryErrorOOM 时自动 dump 堆内存生产必开
-XX:HeapDumpPath指定 dump 文件路径/data/dumps/heap.hprof
-XX:ErrorFileJVM 致命错误日志路径/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 诊断工具
# 查看 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 可以在启动时验证所有参数是否按预期生效,避免配置写错导致的线上问题。

第 6 站

生产就绪:三种场景的 JVM 启动模板

把前 5 站的参数组合起来,针对三种最常见的生产场景,给出可以直接复制使用的启动脚本:

场景一:通用 Web 服务(Spring Boot / 微服务)

web-service.sh — 4GB 堆,G1 GC,延迟优先
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

batch-job.sh — 8GB 堆,Parallel GC,吞吐量优先
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

场景三:超低延迟微服务(金融 / 实时竞价)

low-latency.sh — 16GB 堆,ZGC,亚毫秒停顿
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 日志 → 诊断保护"这四个维度系统回答,并给出推荐值和选型理由,就是一份有经验、有深度的满分回答。