JVM调优参数速查手册:堆大小、GC选择、线程栈的工程配置
JVM调优参数速查手册:堆大小、GC选择、线程栈的工程配置
适读人群:Java中高级开发工程师、运维工程师 | 阅读时长:约20分钟 | 适用JDK版本:JDK 8 / 11 / 17 / 21
开篇故事
我做了十五年Java开发,每次新项目上线,JVM参数都是一个绕不过去的问题。尤其是刚工作的前几年,每次要设JVM参数,我都要翻各种文档,各种拼凑,然后上线后发现参数互相冲突,或者没有起到预期效果。
印象最深的一次是2015年。我把一个项目的JVM参数从同事的笔记里复制过来,包括-XX:+UseConcMarkSweepGC(CMS GC)和-XX:+UseParNewGC,还有各种CMS的参数。上线后用了半年,一直还算正常。
有一天,公司要把这个服务迁移到JDK 11,直接把参数原封不动带过去了。上线后应用崩溃,JVM启动报错:
Unrecognized VM option 'UseConcMarkSweepGC'
Error: Could not create the Java Virtual Machine.CMS GC在JDK 14中被彻底移除,JDK 11中已经废弃(启动会有警告但能运行),但我们的参数里还有一堆CMS专有参数,到JDK 11的某个版本已经不认识了。
仅仅是JVM参数迁移就搞了半天,其中还有好几个参数在JDK 11里行为改变了,最终调试花了整整一天。
这件事让我意识到:一份经过深思熟虑的JVM参数手册对工程团队来说有多重要。今天这篇文章就是我积累15年踩坑经验的总结。
一、JVM参数的分类体系
JVM参数分三类:
标准参数(Standard Options):所有JVM实现都必须支持,格式-option。如-version、-cp、-classpath。稳定,不会轻易变化。
非标准参数(Non-Standard Options):HotSpot JVM特有,以-X开头。如-Xms、-Xmx、-Xss、-Xmn。相对稳定,但不保证所有JVM实现都支持。
不稳定参数(Unstable Options):以-XX:开头,可能随JDK版本变化甚至消失。但实际上大量核心调优参数都是-XX:参数,也是调优最常用的部分。-XX:+表示开启,-XX:-表示关闭,-XX:name=value表示设置值。
二、原理深度解析
2.1 内存相关参数的联动关系
JVM各内存区域的参数之间有复杂的联动关系:
总内存估算公式:
JVM进程总内存 ≈ Xmx + MaxMetaspaceSize + MaxDirectMemorySize
+ (Xss × 线程数) + ReservedCodeCacheSize + JVM自身开销(200~500m)容器化部署时,容器内存限制必须大于这个总和,通常建议:容器内存 = Xmx * 1.5 + 1g作为粗略估算。
2.2 各JDK版本的GC默认值
| JDK版本 | 默认GC | 堆默认值 |
|---|---|---|
| JDK 7 | Parallel GC | 物理内存/4 |
| JDK 8 | Parallel GC | 物理内存/4 |
| JDK 9-11 | G1 GC | 物理内存/4,最大256m |
| JDK 12+ | G1 GC | 物理内存/4,无上限 |
JDK 9+将G1设为默认GC,是里程碑式的变化。如果你的代码是JDK 8的配置,迁移到JDK 11时需要检查GC参数的兼容性。
三、按场景分类的完整参数速查
3.1 基础内存参数
# ===== 堆内存 =====
-Xms4g # 初始堆大小(生产建议与Xmx相同)
-Xmx4g # 最大堆大小
# 经验法则:堆大小 = 服务器内存 × 0.5 ~ 0.7
# 8G内存:堆4~5g
# 16G内存:堆8~10g
# 32G内存:堆16~20g(注意:>32G时指针压缩失效,每个对象头增大)
# ===== 年轻代 =====
# G1/ZGC建议不要手动设置,让GC自动调整
# 如果使用Parallel GC或CMS,可以设置:
-Xmn2g # 固定年轻代大小(或用NewRatio)
-XX:NewRatio=2 # 老年代:年轻代 = 2:1(默认),即年轻代占1/3
# ===== Metaspace =====
-XX:MetaspaceSize=256m # Metaspace扩展触发GC的初始阈值
-XX:MaxMetaspaceSize=512m # Metaspace上限(必须设置!防止无限增长)
# 对于大量使用动态代理的服务(Spring, MyBatis等)
# 建议:MetaspaceSize=256m, MaxMetaspaceSize=512m~1g
# ===== 直接内存 =====
-XX:MaxDirectMemorySize=2g # 直接内存上限
# Netty服务建议设置为2~4g
# 非NIO服务设置为256m~512m即可
# ===== 线程栈 =====
-Xss512k # 每个线程的栈大小(默认512k-1m,平台相关)
# 普通服务:512k(能支撑约200层调用深度,足够)
# 有深递归的服务:1m~2m
# 高并发线程多的服务:256k(线程1000个时,512k就占500m)3.2 GC选择参数
# ===== JDK 8 推荐配置 =====
# 方案A:G1 GC(推荐,堆>4G时首选)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m # 根据堆大小自动计算,也可手动设置
-XX:InitiatingHeapOccupancyPercent=40
# 方案B:Parallel GC(吞吐量优先,不在意停顿时间)
-XX:+UseParallelGC
-XX:ParallelGCThreads=4 # GC线程数,建议=CPU核心数/2
-XX:GCTimeRatio=99 # GC时间占总时间比例的倒数,99=<1%用于GC
# ===== JDK 11 推荐配置 =====
# G1是默认,通常只需要设置停顿目标
-XX:MaxGCPauseMillis=200
# 低延迟需求:ZGC(JDK 11实验版,JDK 15生产就绪)
-XX:+UseZGC # JDK 11需要加 -XX:+UnlockExperimentalVMOptions
# ===== JDK 17/21 推荐配置 =====
# G1(默认,大多数场景推荐)
-XX:MaxGCPauseMillis=200
# 低延迟首选:ZGC(JDK 15+稳定)
-XX:+UseZGC
# JDK 21低延迟最优:分代ZGC
-XX:+UseZGC
-XX:+ZGenerational
# 注意:已废弃/删除的GC
# JDK 9: 废弃CMS (-XX:+UseConcMarkSweepGC显示警告)
# JDK 14: 删除CMS(使用报错)
# JDK 15: 废弃偏向锁
# JDK 17: 删除偏向锁3.3 GC日志参数
# ===== JDK 8 GC日志 =====
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:/var/log/app/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100m
# ===== JDK 9+ 统一日志(推荐)=====
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=50m
# 简洁版(只记录关键GC事件)
-Xlog:gc:file=/var/log/app/gc.log:time:filecount=5,filesize=20m
# 超详细版(调优分析用)
-Xlog:gc*,gc+heap=debug,gc+age=debug:file=/var/log/app/gc_detail.log:time,uptime,level,tags:filecount=5,filesize=50m
# JDK 9+等价于JDK 8的GC日志
-Xlog:gc,gc+heap,gc+age,gc+metaspace:file=/var/log/app/gc.log:time,uptime:filecount=10,filesize=100m3.4 诊断与监控参数
# ===== OOM处理 =====
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump/
-XX:OnOutOfMemoryError="kill -9 %p" # OOM时杀掉进程(让容器重启)
# ===== JMX远程监控 =====
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=<服务器IP>
# ===== 性能诊断 =====
-XX:NativeMemoryTracking=detail # NMT,追踪本地内存(有约5%性能开销)
# 仅在需要分析内存问题时开启
# ===== JFR(推荐长期开启,JDK 11+无额外成本)=====
-XX:+FlightRecorder # JDK 11+默认就有
-XX:StartFlightRecording=dumponexit=true,filename=/var/log/app/app.jfr,settings=profile3.5 JIT编译器参数
# ===== 编译参数 =====
-XX:+TieredCompilation # 分层编译(JDK 8+默认开启)
-XX:ReservedCodeCacheSize=256m # JIT代码缓存大小
-XX:CICompilerCount=4 # 编译线程数(默认=逻辑CPU数)
# ===== 指针压缩(重要!)=====
# 堆<32G时,自动开启指针压缩,每个对象引用4字节而非8字节
# 堆>=32G时,指针压缩失效,对象头变大,内存利用率下降
# 所以通常建议堆不超过28~30G
-XX:+UseCompressedOops # 压缩对象指针(默认开启)
-XX:+UseCompressedClassPointers # 压缩类指针(默认开启)
# ===== 其他常用优化 =====
-XX:+OptimizeStringConcat # String拼接优化(默认开启)
-XX:+UseStringDeduplication # G1 GC特有:字符串去重(有CPU开销,谨慎使用)3.6 按机器规格的推荐配置模板
# ===== 4核8G机器,Java微服务 =====
-Xms4g -Xmx4g
-Xss512k
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:ReservedCodeCacheSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m
# ===== 8核16G机器,Java中型服务 =====
-Xms8g -Xmx8g
-Xss512k
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=2g
-XX:ReservedCodeCacheSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=40
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=10,filesize=50m
# ===== 16核32G机器,Java大型服务 =====
-Xms20g -Xmx20g
-Xss512k
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=1g
-XX:MaxDirectMemorySize=4g
-XX:ReservedCodeCacheSize=512m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=35
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=10,filesize=100m
# ===== 低延迟服务(JDK 21)=====
-Xms16g -Xmx16g
-Xss512k
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+UseZGC
-XX:+ZGenerational
-XX:MaxDirectMemorySize=4g
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=10,filesize=50m四、参数版本兼容性对照表
4.1 各JDK版本的关键变化
| 参数/特性 | JDK 8 | JDK 11 | JDK 17 | JDK 21 |
|---|---|---|---|---|
| 默认GC | Parallel | G1 | G1 | G1 |
-XX:+UseConcMarkSweepGC | 支持 | 废弃(警告) | 不可用 | 不可用 |
| 偏向锁 | 默认开启 | 默认开启 | 已删除 | 已删除 |
-Xloggc | 支持 | 支持(废弃警告) | 废弃 | 废弃 |
-Xlog:gc | 不支持 | 支持 | 支持 | 支持 |
| ZGC | 不支持 | 实验性 | 生产就绪 | 分代ZGC |
| 虚拟线程 | 不支持 | 不支持 | 不支持 | 支持 |
4.2 迁移JDK版本时的参数清理清单
# 从JDK 8迁移到JDK 11,需要删除或替换的参数:
# 删除(JDK 11不支持或行为变化):
-XX:MaxPermSize=... # JDK 8移除(永久代变Metaspace)
-XX:PermSize=... # 同上
# 替换:
# -Xloggc:path → -Xlog:gc*:file=path:time,uptime
# -XX:+PrintGCDetails → 包含在上面的 gc* 中
# -XX:+PrintGCDateStamps → 包含在 time 中
# 从JDK 8/11迁移到JDK 17,需要删除的参数:
-XX:+UseConcMarkSweepGC # CMS已删除
-XX:+UseParNewGC # 随CMS一起删除
-XX:CMSInitiatingOccupancyFraction=... # CMS参数,全部删除
-XX:+CMSParallelRemarkEnabled # 同上
-XX:+UseBiasedLocking # 偏向锁已删除
-XX:BiasedLockingStartupDelay=... # 同上五、踩坑实录
坑一:堆设置为30G指针压缩失效
有台32G内存的机器,想尽量给JVM多一点内存,设置了-Xmx30g。结果发现内存实际利用率没有提升,而且GC停顿时间比-Xmx28g还长。
原因:JVM的压缩对象指针(UseCompressedOops)在堆超过约32GB时自动失效。指针压缩失效后,每个对象引用从4字节变成8字节,对象内存占用增加(尤其是引用密集的对象,如HashMap的Entry),导致相同数量的对象反而占用更多内存,GC需要处理的数据量更大。
建议:堆大小设置在28G以内,确保指针压缩有效。如果确实需要大堆,升级到64G+,让32G-64G这段尴尬区间成为安全距离。
坑二:GC日志没有开启,事故后无法复盘
有一次系统频繁Full GC导致服务不可用,处理时的第一件事是查GC日志,结果发现:没有配置GC日志。处理线上故障,手里没有日志,完全靠猜。
那次花了三倍的时间才定位到问题(内存泄漏)。从此以后,我的所有服务模板里都必须包含GC日志配置,这是不可妥协的基础设施。
坑三:忘记设置MaxDirectMemorySize,进程内存失控
有个Netty写的网关服务,没有配置-XX:MaxDirectMemorySize。服务运行一段时间后,top里看到进程的RSS从6G慢慢涨到12G,而堆只有4G,明显是直接内存在增长。
没有上限约束,直接内存可以无限增长(理论上等于Xmx,实际可以超),直到吃掉所有机器内存导致OOM Killer介入。
从此所有使用Netty或NIO的服务,都必须显式设置-XX:MaxDirectMemorySize。
坑四:-Xss设置太小导致StackOverflowError
服务迁移到容器后,为了能部署更多实例,把-Xss从512k改成了128k。结果生产报了StackOverflowError,而测试环境从未出现过。
原因:生产环境有个代码路径会处理嵌套很深的JSON(最多嵌套200层),128k的栈深度不够支撑这么深的递归调用。测试数据都是简单的JSON,触发不了这个问题。
最终把-Xss改回了256k(512k也可以),同时在代码里加了JSON嵌套深度限制(maxNestingDepth=100)。
六、总结
JVM参数调优没有万能公式,但有可以遵循的工程准则:
原则一:生产最小配置清单。每个Java服务都必须配置:-Xms/-Xmx(相同值)、-XX:MaxMetaspaceSize、-XX:MaxDirectMemorySize、GC日志、-XX:+HeapDumpOnOutOfMemoryError。这五类是不可妥协的基础配置。
原则二:理解参数间的依赖和冲突。特别是内存参数的联动——堆+非堆+直接内存+栈+JVM自身必须小于容器/机器内存。设置任何一个参数前,先算总账。
原则三:JDK版本迁移时必须审查参数。CMS在JDK 14删除,偏向锁在JDK 17删除,GC日志格式在JDK 9改变。每次升级JDK,都要做一次参数兼容性审查。
原则四:不要把调优参数和功能配置混在一起。GC相关参数、内存大小参数、诊断参数应该分组管理,每次变更有记录,可追溯。
