Skip to content

JDK 17 生产环境常用 JVM 参数解析

线上 JVM 参数很容易陷入两个极端:要么完全沿用默认值,要么照抄一套“别人线上在用”的配置。前者未必适合自己的业务负载,后者则可能把别人的约束、风险和历史兼容包袱一起搬过来。

这篇文章不打算给出一套万能参数,而是借一套真实的生产配置,说明在 JDK 17、容器化部署、2C4G 资源规格 下,常见 JVM 参数分别解决什么问题、默认值是什么、改动之后会带来哪些收益和代价。

先看结论

这套参数的核心目标可以概括成 5 点:

  • ZGC 降低停顿时间,优先保证延迟稳定性,而不是追求极致吞吐。
  • MaxRAMPercentageInitialRAMPercentage 把堆固定在较高水位,减少运行期扩容抖动。
  • MaxDirectMemorySizeMaxMetaspaceSizeReservedCodeCacheSize 给 native memory 设边界,避免容器内存失控。
  • -XX:-OmitStackTraceInFastThrow 和独立日志文件提高问题排查能力。
  • 用大量 --add-opens 兼容 JDK 17 模块强封装,但这也意味着应用对历史依赖和深反射有一定包袱。

如果你的服务是低延迟接口型应用,这套思路有参考价值;如果你是吞吐优先的离线任务、启动速度敏感的应用,或者依赖已经充分适配 JDK 17,就不要直接照抄。

示例配置

以下是某电商平台核心交易服务的 JVM 配置,单个容器配置为 2C4G,JDK 版本为 17.0.10:

bash
-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
  • 默认行为:初始堆较小,运行中按需扩容
  • 调整后的收益:启动后基本不需要再扩堆,减少扩容抖动
  • 代价与风险:启动阶段直接申请较大的堆,启动更慢,初始内存占用更高

InitialRAMPercentageMaxRAMPercentage 都设为 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 相关 tag
  • warning:只记录 warning 及以上级别
  • file=/data/logs/gc%t.log:输出到文件,%t 表示带启动时间戳
  • utctime,level,tags:每条日志带 UTC 时间、级别和 tag
  • filecount=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.langjava.lang.reflectjava.lang.invoke
  • java.iojava.netjava.nio
  • java.mathjava.textjava.time
  • java.utiljava.util.concurrent
  • java.security
  • jdk.internal.accessjdk.internal.misc
  • sun.netsun.net.utilsun.net.www
  • sun.reflect.annotation
  • sun.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 和部分系统属性兼容历史框架与旧依赖

Last updated: