JVM内存调优(更少的内存,做更多的事)
发布于2025-08-10 01:13:14,更新于2025-09-25 01:44:37,标签:java 文章会持续修订,转载请注明来源地址:https://meethigher.top/blog之前有过 JVM 内存分析的相关记录
本文参考内容
- 运维:你们 JAVA 服务内存占用太高,还只增不减!告警了,快来接锅 - 个人文章 - SegmentFault 思否
- JVM实战:CMS和G1的物理内存归还机制_shrinkheapinsteps-CSDN博客
- JEP 346: Promptly Return Unused Committed Memory from G1
一、基础
1.1. JVM内存组成
JVM 内存,分为堆内存和非堆内存。结构组成如下
1.2 JVM GC流程
JVM GC 的流程如下。注意不管是 Minor GC 还是 Full GC,都会在 GC 期间出现 STW(Stop The World)现象。
二、内存调优
记录下内存调优(更少的内存,做更多的事)的步骤
- 查 JVM 默认参数
- 查 GC。确认 STW 耗时可接受
- 查堆。确认对象用量符合预期
- 查非堆。确认非堆用量符合预期
- 配置自适应堆伸缩。忙时扩容堆内存、闲时归还堆内存给操作系统。
2.1 查JVM默认参数
可以在启动前/启动后,分别查看 JVM 的默认(配置)参数
1 | # 启动前,查看JVM默认参数 |
2.2 查GC
确认 STW 耗时可接受
1 | # 查看进程的堆内存分布,及GC情况。不要求与启动程序的 java 版本一致 |
该命令受到
/tmp/hsperfdata_<user>/<pid>
影响,若该文件被清除掉,则无法使用相关命令。目前经我测试,jvm还没有暴露一个参数可以进行控制保存路径。 参考[JDK-6447182] temp dir locations should not be hardcoded for hsperfdata_
dirctories - Java Bug System
2.3 查堆
确认对象用量符合预期。
1 | # 查看进程的堆内存分布,不要求与启动程序的 java 版本一致 |
其中使用到 MAT 可视化分析,下载地址Downloads | The Eclipse Foundation,适用于小白。老手只使用 jmap 也能达到一样达到效果。
2.4 查非堆
确认非堆用量符合预期。
1 | # 开启内存跟踪,支持summary/detail模式,summary对应用本身的性能影响不大 |
2.5 配置自适应扩缩堆
这里面有几个问题。要记录下。
- 堆内存、操作系统虚拟内存、操作系统物理内存之间的关系?
- 答:JVM 启动后,会向操作系统申请(commited)
-Xms
大小的虚拟内存作为堆内存。只有当真实使用时,操作系统才会分配物理内存。这会出现一种现象,操作系统分配给 JVM 进程的物理内存,会小于-Xms
的情况。
- 答:JVM 启动后,会向操作系统申请(commited)
- 什么叫扩缩堆?
- 答:JVM 运行时,发现当前的堆内存不够用了,并且当前堆内存的分配还没有达到
-Xmx
,就会向操作系统继续申请虚拟内存。这就是堆内存的扩容;当足够空闲时, JVM 就会向操作系统归还部分堆内存,这就是堆内存的收缩。
- 答:JVM 运行时,发现当前的堆内存不够用了,并且当前堆内存的分配还没有达到
2.5.1 参数及示例
忙时扩容堆内存、闲时归还堆内存给操作系统。
扩堆是所有 GC 都支持的。
这里面主要记录缩堆,不是所有的 GC 都支持缩堆。
开启缩堆也能提升 Jetbrains 家的产品性能。
严格来说,所有 JVM 程序,开启缩堆之后,都可以在非内存密集场景中,发挥更大的价值。
开启缩堆我是使用的 G1GC。以 jdk8 为例,默认的 ParallelGC 是不支持将空闲的堆内存归还给操作系统的,这就会出现一种占着茅坑不拉屎的情况。
下面我基于个人的理解与实践(meethigher/jvm-heap-memory-simulator: jvm heap memory 测试工具,用于测试 jvm gc),记录常见的 jdk 版本使用 G1GC 时对于缩堆归还内存的支持度。
LTS 版本 | 缩堆(归还内存)触发时机 |
---|---|
jdk 8/11 | FullGC |
jdk 17/21 | FullGC、PeriodicGC |
如果是对内存的利用率有明确要求的话,那就考虑支持 PeriodicGC 的 jdk 版本。根据官方说明来看,jdk12 以后都支持。参考JEP 346: Promptly Return Unused Committed Memory from G1
这里面有几个关键参数。
- -Xms
- JVM 启动时堆内存的初始大小。默认为物理内存大小的1/64
- -Xmx
- JVM 堆内存的最大值。默认为物理内存大小的1/4
- -XX:+UseG1GC
- 启用 G1 垃圾收集器。将堆划分为多个 region(区域),通过预测 GC 停顿时间来选择回收顺序。对大内存堆性能友好,减少 Full GC 的停顿。默认在 JDK 9+ 中启用。
- -XX:MinHeapFreeRatio
- 堆中最小空闲内存比例(百分比)。如果堆中空闲内存低于该值,JVM 会考虑扩展堆。jdk11 及之后默认值是 40
- -XX:MaxHeapFreeRatio
- 堆中最大空闲内存比例(百分比)。如果堆中空闲内存高于该值,JVM 会考虑收缩堆。jdk 11 及之后默认值是 70
- -XX:G1PeriodicGCInterval
- G1GC 中 periodic GC 的间隔时间(毫秒)。参考JEP 346: Promptly Return Unused Committed Memory from G1
- -XX:G1PeriodicGCSystemLoadThreshold
- G1GC 中 periodic GC 的系统负载阈值。如果系统负载超过该值,G1 会跳过 periodic GC。参考JEP 346: Promptly Return Unused Committed Memory from G1
- 开启GC日志
- jdk8 及之前:
-XX:+PrintGCDetails
- jdk9 及之后:
-Xlog:gc*=debug:file=gc.log:time,level,tags:filecount=5,filesize=100M
- jdk8 及之前:
下面放一个启动示例。
1 | java -Xms512m -Xmx40g -XX:+UseG1GC \ |
2.5.2 openjdk周期gc存在bug
我在查看 openjdk 21 的源码时,发现如下问题。
该问题会导致在 windows 上面 periodic GC 的配置参数失效。本来我想提个 issue 和 pr 来着,发现已经有小伙伴提了,比我早发现了一个星期,可惜了。该问题会在 jdk 26 上面解决,不过只是修改了判定逻辑,因为 loadavg 的逻辑没改,因此在 windows 上依然不支持。参考[JDK-8368089] G1: G1PeriodicGCTask::should_start_periodic_gc may use uninitialised value if os::loadavg is unsupported - Java Bug System
2.5.3 个人一些抛弃jdk8的理由
以下现象可以自己写代码来模拟。
抛弃 jdk8,我的理由有两个
- oom 问题。举个例子,在 jdk8 中,如果垃圾已经占用堆内存 50%,并且还未触发 GC,这时候直接一次性来堆内存 50% 的临时对象创建,程序直接 oom,并且宕机不可用、不可恢复。在 jdk11 及之后的版本中,同样的场景,会触发 GC 先回收掉垃圾,再创建对象。不会出现 oom。
- 缩堆不够高效。在 jdk8/11 中,缩堆只能通过 full gc 来回收,但是 full gc 停顿的时间也是相当长的,像我的简单业务都有 200ms 的停顿。因此缩堆要尽量避免 full gc,那么就要考虑支持 periodic gc 的版本了(periodic gc 只是 gc 的触发源,具体触发哪种 gc 由 g1 决定,但是比 full gc 要更高效的)。