2558 字
约 8 分钟
21
S05-X03 · JVM 启动参数清单:堆、GC、转储与诊断开关
2024-12-31
2026-11-08

新机器上线,启动脚本里的 -Xmx 通常是从上一个项目抄来的。它可能来自一台 64G 的物理机,现在却跑在 4G limit 的容器里。参数本身没有对错,错的是它和当前的机器规格、GC 算法、可观测性需求不匹配。

这篇按四个决策点排一遍:堆开多大 → 用哪个收集器 → 出事时留下什么 → 平时怎么核对。每个参数给 JDK 17 下的默认值与失效条件,最后一张常见错配表。


1. 堆:三个数决定内存上限

堆的边界只有三个关键数:初始值、最大值、新生代占比。其余参数都是在这三个数划定的框里做微调。

$ java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E ' (Initial|Max)HeapSize|MaxRAMPercentage'
   size_t InitialHeapSize            = 268435456  {product} {ergonomic}
   size_t MaxHeapSize                = 4294967296 {product} {ergonomic}
   double MaxRAMPercentage           = 25.000000  {product} {default}

{ergonomic} 表示这个值由 JVM 自己算出来,不是你写的。上面这台机器物理内存 16G,所以 MaxHeapSize 落在 4G(1/4)。

参数 含义 JDK 17 默认 什么时候必须显式写
-Xms 初始堆大小 物理内存的 1/64 想避免启动后反复扩堆
-Xmx 最大堆 物理内存的 1/4,受容器 limit 约束 生产环境一律显式写
-Xmn 新生代固定大小 未设置,按 NewRatio 或自适应 只有 Parallel/Serial 下才建议用
-XX:NewRatio 老年代/新生代比值 2 用 Parallel 且要调大新生代时
-XX:SurvivorRatio Eden/Survivor 比值 8 观测到 Survivor 溢出时
-XX:MaxRAMPercentage 堆占可用内存百分比 25.0 容器里替代 -Xmx

-Xms 与 -Xmx 设成相等是常规做法:堆不再动态伸缩,避免运行期扩堆带来的停顿与 RSS 抖动。代价是启动即占满,容器里要把这部分算进 limit。

优先级要记牢:-Xmx 高于 MaxRAMPercentage。两个都写时 -Xmx 生效,百分比被忽略。这是排查"为什么改了百分比没反应"的第一个原因。

2. 容器感知:为什么 limit 是 2G,堆却开了 4G

JDK 10 起 -XX:+UseContainerSupport 默认开启(8u191 起回移到 8),JVM 不再读宿主机 /proc/meminfo,而是读 cgroup 的内存限制。JDK 17 同时支持 cgroup v1 与 v2。

# cgroup v2 下确认容器真实上限
$ cat /sys/fs/cgroup/memory.max
2147483648

容器里推荐两种写法,二选一,不要混:

# 写法 A:绝对堆
java -Xms2g -Xmx2g -jar app.jar

# 写法 B:百分比,给不同规格的实例复用同一份脚本
java -XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70 -jar app.jar

MaxRAMPercentage=70 不是保守,而是必需:堆之外还有元空间、线程栈、代码缓存、GC 自身的辅助结构、Direct Buffer、JNI 分配。这些都不在 -Xmx 里。

组成 大致规模 是否受 -Xmx 约束
Java 堆 -Xmx 指定 是
Metaspace 数十 MB 起,无上限则随类数增长 否
线程栈 每线程约 1MB(-Xss) 否
Code Cache 默认上限 240MB 否
Direct Buffer 默认等于 -Xmx 上限 否
GC 辅助结构 堆的百分之几 否

最后一行是坑:-XX:MaxDirectMemorySize 默认值等于最大堆。一个 4G 堆的服务如果再吃 4G 堆外,RSS 就是 8G 起步。

3. 新生代:-Xmn 与 NewRatio 的取舍

-Xmn 直接把新生代钉死,NewRatio 按比例推导。两者互斥,写了 -Xmn 就忽略 NewRatio。

# Parallel GC:新生代 1G,老年代 3G(堆 4G)
java -Xms4g -Xmx4g -Xmn1g -XX:+UseParallelGC -jar app.jar

G1 下不要写 -Xmn。G1 的新生代大小是自适应的,它靠调整 young region 数量来命中 MaxGCPauseMillis。钉死新生代等于把 G1 唯一的手脚捆住,停顿目标随之失效。G1 场景要表达"我想要多短的停顿",写这个(MaxGCPauseMillis 是目标不是承诺,追不上时日志里会出现 to-space exhausted):

java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
     -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -jar app.jar

4. Metaspace:唯一没有默认上限的内存区

JDK 8 移除永久代后,类的元数据搬到了本地内存。-XX:MaxMetaspaceSize 默认不限,意味着它能一直吃到把机器吃死——而 -Xmx 完全管不到它。

$ jcmd 5123 VM.metaspace
# 典型输出形态(按 JDK 17 核对字段名)
Metaspace Used: 89 MB, Committed: 96 MB, Reserved: 1056768 KB
参数 作用 默认 建议
-XX:MaxMetaspaceSize 元空间硬上限 不限 生产写 256m–512m
-XX:MetaspaceSize 首次触发元空间 GC 的水位 约 21MB 一般不用动
-XX:CompressedClassSpaceSize 压缩类空间 1G 类极多时留意
-XX:+ClassUnloadingWithConcurrentMark G1 并发标记阶段卸载类 开启 保持开启

上限设了以后,超出的报错是这一行:

java.lang.OutOfMemoryError: Metaspace

这个报错和 Java heap space 的处理路径完全不同:堆 OOM 要去查对象,Metaspace OOM 要去查类是谁加载的——动态代理、CGLIB、脚本引擎、热部署残留的类加载器。设上限的价值就在于把它变成一个可定位的错误,而不是一次安静的 OOMKilled。

5. GC 选择:三种主流收集器的适用面

收集器 开关 停顿特征 适用 代价
G1 -XX:+UseG1GC(JDK 9+ 默认) 可设目标,秒级以内 4G–32G 通用服务 吞吐略低于 Parallel
ZGC -XX:+UseZGC 亚毫秒级,与堆大小弱相关 大堆(数十 G)、低延迟 吞吐损失、内存占用高
Parallel -XX:+UseParallelGC 全程 STW,停顿随堆增长 批处理、离线计算 延迟不可控
Serial -XX:+UseSerialGC 单线程 STW 小堆(<1G)、客户端 不适合服务端

选择逻辑很简单:先问能不能接受停顿。离线跑批只关心总时间,Parallel 吞吐最高;在线接口关心 P99,堆又不大,G1 够用;堆到几十 G 还想稳,ZGC。ZGC 在 JDK 15 转正,JDK 17 下不需要 UnlockExperimentalVMOptions;分代 ZGC 是 JDK 21 才有的事,别在 17 上找 -XX:+ZGenerational。

# ZGC 一组常用参数
java -Xms16g -Xmx16g -XX:+UseZGC -XX:+ZUncommit -XX:ZUncommitDelay=300 \
     -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=50m \
     -jar app.jar

6. GC 日志:统一日志(-Xlog)唯一正确写法

JDK 9 之后 -XX:+PrintGCDetails 那套全部废弃,只剩 -Xlog。格式是 -Xlog:选择器:输出:装饰器:滚动。

-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20m
# 先试跑,确认格式再上生产
$ java -Xlog:gc* -version 2>&1 | head -3
[0.012s][info][gc] Using G1

7. OOM 时留下现场

没有转储的 OOM,事后只能靠猜。三个开关解决"死了以后还能查"。

java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/data/dump \
     -XX:+ExitOnOutOfMemoryError \
     -XX:+PrintFlagsFinal \
     -jar app.jar
开关 行为 适用场景
-XX:+HeapDumpOnOutOfMemoryError OOM 时导出 hprof 几乎必开
-XX:HeapDumpPath 指定目录或文件名 目录要留足空间
-XX:+ExitOnOutOfMemoryError 首次 OOM 直接退出(非 0 码) 有编排系统拉起时
-XX:+CrashOnOutOfMemoryError OOM 时 abort 并生成 core 需要 native 现场时
HeapDumpPath 指向目录时,文件名是 java_pid<pid>.hprof;指向一个不存在的目录不会报错,只是转储静默失败——这是最隐蔽的坑。转储期间 JVM 处于 STW,堆 8G 就要写 8G 文件。

ExitOnOutOfMemoryError 的语义要看清:它让进程退出,把"重启"交给外面的编排。如果进程被 supervisor 原地拉起而你又不看退出码,表现就是"服务每隔几分钟抖一下",比留着半死状态更难查。

8. 诊断开关:核对实际生效值

参数写了不等于生效。-XX:+PrintFlagsFinal 打印最终值,含默认值、ergonomic 推导、被后续参数覆盖的结果。

# 启动时打印全部最终值(建议只在排障时开)
$ java -XX:+PrintFlagsFinal -XX:+PrintCommandLineFlags -jar app.jar

# 线上不重启也能读
$ jcmd 5123 VM.flags
$ jcmd 5123 VM.command_line
$ jcmd 5123 VM.native_memory summary   # 需启动时加 -XX:NativeMemoryTracking=summary

9. 排错:五种典型错配

现象 原因 处理
容器退出码 137,日志无 OOM 堆 + 堆外 > cgroup limit,被内核 OOMKilled 降 -Xmx 或提 limit,堆外单独设上限
改了 MaxRAMPercentage 无效 同时显式写了 -Xmx 去掉 -Xmx,或改成百分比写法
Full GC 每次停 2s 以上 堆过大且用 Parallel/未设停顿目标 换 G1 + MaxGCPauseMillis,或减小堆
OutOfMemoryError: Metaspace 动态类无限增长或 MaxMetaspaceSize 过小 查类加载器泄漏,再评估上限
写了 HeapDumpPath 却没有 hprof 目录不存在或不可写,转储静默失败 启动前 mkdir -p 并验证权限

137 那个值得单独说:OOMKilled 是内核杀的,JVM 来不及打任何日志,-XX:+HeapDumpOnOutOfMemoryError 也不会触发。看到 137 就该去看 cgroup 的 memory.events,而不是翻 GC 日志。

10. 边界:参数解决不了的部分

问题 参数能否解决 该做什么
内存泄漏 不能 堆快照 + 引用链定位
单次请求创建大对象 只能缓解 改代码,减少临时对象
线程池队列积压 不能 有界队列 + 拒绝策略
容器 CPU 限流导致变慢 部分 -XX:ActiveProcessorCount 对齐配额
GC 日志看不懂 不能 配可视化工具,或补 gc+phases

-XX:ActiveProcessorCount 是容器时代少数值得记住的补充参数:JVM 默认按宿主机核数算 GC 线程与 ForkJoinPool 并行度,cgroup 只给 2 核时,按 32 核推导出来的线程数只会互相抢 CPU。

11. 自测

  1. -Xmx 与 -XX:MaxRAMPercentage 同时存在时,哪个生效?
  2. 容器 limit 2G,-Xmx2g 为什么会退出码 137?
  3. G1 下设置 -Xmn 会破坏什么机制?
  4. OutOfMemoryError: Metaspace 应该先查什么,而不是先调大上限?
  5. -Xlog:gc 与 -Xlog:gc* 的差别在哪里?
  6. -XX:HeapDumpPath 指向不存在的目录,现象是什么?
S05-X03 · JVM 启动参数清单:堆、GC、转储与诊断开关
https://www.clxhxhhr.top/posts/5646/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。