JDK 17 生产环境常用 JVM 参数解析
线上 JVM 参数很容易陷入两个极端:要么完全沿用默认值,要么照抄一套“别人线上在用”的配置。前者未必适合自己的业务负载,后者则可能把别人的约束、风险和历史兼容包袱一起搬过来。
这篇文章不打算给出一套万能参数,而是借一套真实的生产配置,说明在 JDK 17、容器化部署、2C4G 资源规格 下,常见 JVM 参数分别解决什么问题、默认值是什么、改动之后会带来哪些收益和代价。
先看结论
这套参数的核心目标可以概括成 5 点:
- 用
ZGC降低停顿时间,优先保证延迟稳定性,而不是追求极致吞吐。 - 用
MaxRAMPercentage和InitialRAMPercentage把堆固定在较高水位,减少运行期扩容抖动。 - 用
MaxDirectMemorySize、MaxMetaspaceSize、ReservedCodeCacheSize给 native memory 设边界,避免容器内存失控。 - 用
-XX:-OmitStackTraceInFastThrow和独立日志文件提高问题排查能力。 - 用大量
--add-opens兼容 JDK 17 模块强封装,但这也意味着应用对历史依赖和深反射有一定包袱。
如果你的服务是低延迟接口型应用,这套思路有参考价值;如果你是吞吐优先的离线任务、启动速度敏感的应用,或者依赖已经充分适配 JDK 17,就不要直接照抄。
示例配置
以下是某电商平台核心交易服务的 JVM 配置,单个容器配置为 2C4G,JDK 版本为 17.0.10:
-XX:-OmitStackTraceInFastThrow
-Xlog:gc*=warning:file=/data/logs/gc%t.log:utctime,level,tags:filecount=10,filesize=100M
-Xlog:jit+compilation=info:file=/data/logs/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M
-XX:MaxRAMPercentage=55
-XX:InitialRAMPercentage=55
-XX:+AlwaysPreTouch
-XX:MaxDirectMemorySize=512m
-XX:MaxMetaspaceSize=512m
-XX:ReservedCodeCacheSize=256m
-XX:+DisableExplicitGC
-XX:+UseZGC
-XX:-UseBiasedLocking
-Dspring.amqp.deserialization.trust.all=true
-Dlog4j2.formatMsgNoLookups=true
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/jdk.internal.access=ALL-UNNAMED
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/sun.net=ALL-UNNAMED
--add-opens java.base/sun.net.util=ALL-UNNAMED
--add-opens java.base/sun.reflect.annotation=ALL-UNNAMED
--add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED
--add-exports java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-reads java.base=ALL-UNNAMED
--add-opens java.base/java.lang.invoke=ALL-UNNAMED
-Ddc.access.maxFileSize=128MB
--add-opens java.base/sun.net.www=ALL-UNNAMED从目的上看,这批参数大致可以分成 5 类:
- GC 与堆内存
- Native Memory 边界控制
- 日志与诊断
- JDK 17 模块兼容
- 安全与应用系统属性
GC 与堆内存
-XX:+UseZGC
- 默认值:
false - 默认行为:JDK 17 HotSpot Server VM 默认使用
G1 GC,不是ZGC - 调整后的收益:停顿时间通常更低,更适合延迟敏感服务
- 代价与风险:吞吐不一定优于 G1;团队如果缺少 ZGC 运维经验,排障门槛会更高
- 适用场景:大堆、低延迟、接口型服务
-XX:MaxRAMPercentage=55
- 默认值:通常为
25.0 - 默认行为:最大堆按容器或机器可用内存的约 25% 计算
- 调整后的收益:给 Java 堆更多空间,降低因堆偏小导致的 GC 压力
- 代价与风险:会压缩 direct memory、metaspace、线程栈、JNI、本地库等 native memory 的余量
在 2C4G 容器里,55% 意味着最大堆大约在 2.2G 左右。看起来还剩下将近一半内存,但这部分并不是“可随便使用的空闲区”,因为 JVM 进程还要和以下几类内存一起竞争容器上限:
- 直接内存
- 元空间
- Code Cache
- 线程栈
- GC 自身开销
- 本地库和操作系统页缓存
如果容器 limit 本来就紧,这种配置更容易逼近 OOMKilled 边界。
-XX:InitialRAMPercentage=55
- 默认值:通常为
1.5625 - 默认行为:初始堆较小,运行中按需扩容
- 调整后的收益:启动后基本不需要再扩堆,减少扩容抖动
- 代价与风险:启动阶段直接申请较大的堆,启动更慢,初始内存占用更高
当 InitialRAMPercentage 和 MaxRAMPercentage 都设为 55 时,本质上相当于把堆初始值和最大值拉到了同一水位附近。这种做法的思路很直接:用更慢的启动和更高的初始占用,换取运行期更稳定的内存曲线。
-XX:+AlwaysPreTouch
- 默认值:
false - 默认行为:JVM 不会在启动时把堆页全部预触碰
- 调整后的收益:把缺页中断和页映射的开销前置到启动阶段,运行期访问内存更平稳
- 代价与风险:启动显著变慢,大堆场景下启动阶段 CPU 和内存压力会更高
它通常和固定堆、低延迟目标一起出现。如果你的应用更在意快速启动,而不是长时间稳定运行,这个参数未必划算。
Native Memory 边界控制
-XX:MaxDirectMemorySize=512m
- 默认值:未显式配置时,由 JVM 和类库按运行时条件推导,实践中常见为接近堆上限,但不要简单等同于
Xmx - 默认行为:直接内存没有一个你手动指定的硬编码值
- 调整后的收益:限制 Netty、NIO、ByteBuffer 等堆外内存的上限,便于做容量规划
- 代价与风险:如果应用对直接内存依赖较重,可能报
OutOfMemoryError: Direct buffer memory
-XX:MaxMetaspaceSize=512m
- 默认值:不设硬上限
- 默认行为:Metaspace 按需增长,直到受宿主机或容器原生内存限制
- 调整后的收益:对类加载器泄漏、动态代理过多等问题提供硬边界
- 代价与风险:类很多、代理很多、动态生成类较多时,可能报
OutOfMemoryError: Metaspace
-XX:ReservedCodeCacheSize=256m
- 默认值:JDK 17 HotSpot 常见默认值约为
240MB,不同平台和构建可能略有差异 - 默认行为:JIT 编译后的机器码缓存略小于当前配置
- 调整后的收益:热点方法较多、JIT 编译积极时,更不容易出现
CodeCache is full - 代价与风险:会额外占用一点 native memory
-XX:+DisableExplicitGC
- 默认值:
false - 默认行为:
System.gc()默认有效 - 调整后的收益:避免业务代码、第三方库或历史组件触发显式 GC,减少不必要停顿
- 代价与风险:如果某个组件依赖显式 GC 释放堆外资源,表现可能和预期不一致
这一组参数的共同思路不是“把 JVM 调得更大”,而是“给堆外内存设边界,避免在容器环境里把 native memory 用到失控”。
日志与诊断
-Xlog:gc*=warning:file=/data/logs/gc%t.log:utctime,level,tags:filecount=10,filesize=100M
这条参数使用的是 JDK 9 之后的统一日志系统。含义如下:
gc*:匹配所有 GC 相关 tagwarning:只记录warning及以上级别file=/data/logs/gc%t.log:输出到文件,%t表示带启动时间戳utctime,level,tags:每条日志带 UTC 时间、级别和 tagfilecount=10,filesize=100M:滚动保留 10 个文件,每个最大 100MB
需要特别强调的是,这不是一份“详细 GC 日志”。因为级别是 warning,它只会在 GC 相关告警出现时提供信息,通常不足以分析常规 GC 行为。如果你的目标是排查堆使用、回收周期和停顿耗时,更适合把级别调到 info。
-Xlog:jit+compilation=info:file=/data/logs/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M
- 默认值:没有这条专门配置
- 默认行为:JIT 编译信息不会独立落盘
- 调整后的收益:便于分析热点方法的编译、去优化和重编译情况
- 代价与风险:增加少量日志 IO,日常价值通常不如 GC 日志直接
-XX:-OmitStackTraceInFastThrow
- 默认值:FastThrow 优化默认开启,也就是热点重复异常可能省略完整堆栈
- 默认行为:某些异常在高频抛出后,JVM 可能不再打印完整 stack trace
- 调整后的收益:重复异常依然保留完整堆栈,问题定位更容易
- 代价与风险:异常频繁时,会增加堆栈构造和日志输出的开销
这个参数属于典型的“用一点性能换可观测性”。
JDK 17 模块兼容参数
JDK 9 之后引入模块系统,JDK 17 又进一步强化了封装边界。很多老框架、字节码增强库和深反射依赖在升级后会遇到兼容问题,因此才会出现大量 --add-opens、--add-exports 和 --add-reads。
--add-opens java.base/...=ALL-UNNAMED
- 默认值:无
- 默认行为:JDK 内部很多包默认不允许被深反射访问
- 调整后的收益:让 Spring、Jackson、Hibernate、Netty、CGLIB 或其他历史依赖继续通过反射访问这些包
- 代价与风险:破坏模块封装,后续升级 JDK 或替换依赖时更脆弱
这份配置里开放了下面这些包:
java.lang、java.lang.reflect、java.lang.invokejava.io、java.net、java.niojava.math、java.text、java.timejava.util、java.util.concurrentjava.securityjdk.internal.access、jdk.internal.miscsun.net、sun.net.util、sun.net.wwwsun.reflect.annotationsun.reflect.generics.reflectiveObjects
这说明应用或其依赖对 JDK 内部 API 和深反射的依赖比较重。
--add-exports java.base/sun.security.action=ALL-UNNAMED
- 默认值:无
- 默认行为:内部包默认不导出,外部代码不能直接访问
- 调整后的收益:允许依赖继续使用这个内部 API
- 代价与风险:对 JDK 内部实现形成显式耦合,未来兼容性较差
--add-reads java.base=ALL-UNNAMED
- 默认值:无额外读取关系
- 默认行为:模块读取关系由标准模块图决定
- 调整后的收益:解决少数历史兼容问题
- 代价与风险:进一步打破标准模块边界,让问题定位更困难
这里还有两个值得点出来的细节:
--add-opens java.base/java.security=ALL-UNNAMED出现了两次,属于重复配置。-XX:-UseBiasedLocking在 JDK 17 中已经接近历史遗留参数,现实价值很低,不建议在新文章里把它当成重点调优项。
安全与应用系统属性
-Dspring.amqp.deserialization.trust.all=true
这不是 JVM 调优参数,而是 Spring AMQP 的系统属性。
- 默认值:不是
true - 默认行为:默认不会无条件信任所有反序列化类型
- 调整后的收益:兼容性最省事,消息里带什么类就反序列化什么类
- 代价与风险:如果消息来源不完全可信,会引入明显的反序列化安全风险
生产环境里,更推荐使用受控包白名单,而不是直接 trust all。
-Dlog4j2.formatMsgNoLookups=true
这同样不是 JVM 参数,而是 Log4j2 的系统属性。
- 默认值:是否需要依赖它,取决于 Log4j2 版本
- 历史背景:它主要用于对旧版本 Log4j2 的消息查找风险做兼容性防护
- 现实意义:如果你的 Log4j2 已经升级到安全版本,这个参数通常不是必须,但保留它的副作用也很小
-Ddc.access.maxFileSize=128MB
这属于业务应用自定义系统属性,不属于 JVM 标准参数。
- 默认值:取决于应用代码
- 调整后的收益:通常用于控制上传、导入或访问文件的大小限制
- 代价与风险:配置过小会限制业务能力,配置过大则可能增加内存和 IO 压力
这一类属性最好单独归类,不要和 GC、堆内存、模块兼容参数混为一谈。
这套参数适合什么场景
这套配置本质上是一种“低停顿、强约束、偏稳定性优先”的思路,更适合以下场景:
- 容器化部署的 Java 服务
- 延迟敏感的线上接口型应用
- 运行时间长、启动次数少的服务
- 更关注运行稳定性和问题排查效率,而不是极致吞吐的场景
- 仍然存在部分历史依赖,需要通过 --add-opens 等参数兼容 JDK 17 的应用
从设计目标看,这套参数主要是在做几件事:
- 用 ZGC 换取更低的停顿时间
- 用较高的固定堆和 AlwaysPreTouch 换取更平稳的运行期内存表现
- 用多个上限参数约束 native memory,避免容器内存失控
- 用 GC/JIT 日志和完整异常堆栈提升排障能力
- 用 --add-opens 和部分系统属性兼容历史框架与旧依赖