言成言成啊 | Kit Chen's Blog

JVM内存调优(更少的内存,做更多的事)

发布于2025-08-10 01:13:14,更新于2025-09-25 01:44:37,标签:java  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

之前有过 JVM 内存分析的相关记录

本文参考内容

一、基础

1.1. JVM内存组成

JVM 内存,分为堆内存和非堆内存。结构组成如下

1.2 JVM GC流程

JVM GC 的流程如下。注意不管是 Minor GC 还是 Full GC,都会在 GC 期间出现 STW(Stop The World)现象。

二、内存调优

记录下内存调优(更少的内存,做更多的事)的步骤

  1. 查 JVM 默认参数
  2. 查 GC。确认 STW 耗时可接受
  3. 查堆。确认对象用量符合预期
  4. 查非堆。确认非堆用量符合预期
  5. 配置自适应堆伸缩。忙时扩容堆内存、闲时归还堆内存给操作系统。

2.1 查JVM默认参数

可以在启动前/启动后,分别查看 JVM 的默认(配置)参数

1
2
3
4
5
# 启动前,查看JVM默认参数
java -XX:+PrintFlagsFinal -version

# 启动后,查看JVM默认参数
jcmd <pid> VM.flags -all

2.2 查GC

确认 STW 耗时可接受

1
2
3
# 查看进程的堆内存分布,及GC情况。不要求与启动程序的 java 版本一致
# 大小单位为KB,时间单位为S
jstat -gc <pid>

该命令受到/tmp/hsperfdata_<user>/<pid>影响,若该文件被清除掉,则无法使用相关命令。

目前经我测试,jvm还没有暴露一个参数可以进行控制保存路径。 参考[JDK-6447182] temp dir locations should not be hardcoded for hsperfdata_dirctories - Java Bug System

2.3 查堆

确认对象用量符合预期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看进程的堆内存分布,不要求与启动程序的 java 版本一致
jcmd <pid> GC.heap_info

# 查看进程的堆内存分布,要求与启动程序的 java 版本一致
# jdk8 命令
jmap -heap <pid>
# jdk8 以后的命令
jhsdb jmap --pid <pid> -heap

# 只统计存活对象的数量和大小,输出文本报告
jmap -histo:live <pid>

# 把存活对象完整地拷贝到二进制heap dump文件里。供MAT离线分析
jmap -dump:live,format=b,file=dump.bin <pid>

其中使用到 MAT 可视化分析,下载地址Downloads | The Eclipse Foundation,适用于小白。老手只使用 jmap 也能达到一样达到效果。

2.4 查非堆

确认非堆用量符合预期。

1
2
3
4
5
6
7
8
9
10
# 开启内存跟踪,支持summary/detail模式,summary对应用本身的性能影响不大
java -XX:NativeMemoryTracking=summary -jar test.jar

# 查看内存分布
jcmd <pid> VM.native_memory summary

# 设置基准线,然后查看内存增量
jcmd <pid> VM.native_memory baseline
# 盯 +/- 号,这里面记录内存增与减的部分
jcmd <pid> VM.native_memory summary.diff

2.5 配置自适应扩缩堆

这里面有几个问题。要记录下。

  1. 堆内存、操作系统虚拟内存、操作系统物理内存之间的关系?
    • 答:JVM 启动后,会向操作系统申请(commited)-Xms 大小的虚拟内存作为堆内存。只有当真实使用时,操作系统才会分配物理内存。这会出现一种现象,操作系统分配给 JVM 进程的物理内存,会小于 -Xms 的情况。
  2. 什么叫扩缩堆?
    • 答:JVM 运行时,发现当前的堆内存不够用了,并且当前堆内存的分配还没有达到 -Xmx,就会向操作系统继续申请虚拟内存。这就是堆内存的扩容;当足够空闲时, 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/11FullGC
jdk 17/21FullGC、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
  • -XX:G1PeriodicGCSystemLoadThreshold
  • 开启GC日志
    • jdk8 及之前:-XX:+PrintGCDetails
    • jdk9 及之后:-Xlog:gc*=debug:file=gc.log:time,level,tags:filecount=5,filesize=100M

下面放一个启动示例。

1
2
3
4
5
6
7
8
java -Xms512m -Xmx40g -XX:+UseG1GC \
-XX:MinHeapFreeRatio=10 \
-XX:MaxHeapFreeRatio=20 \
-XX:G1PeriodicGCInterval=3600000 \
-XX:G1PeriodicGCSystemLoadThreshold=20 \
-XX:NativeMemoryTracking=summary \
-Xlog:gc*=debug:file=gc.log:time,level,tags:filecount=5,filesize=100M \
-jar test.jar

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,我的理由有两个

  1. oom 问题。举个例子,在 jdk8 中,如果垃圾已经占用堆内存 50%,并且还未触发 GC,这时候直接一次性来堆内存 50% 的临时对象创建,程序直接 oom,并且宕机不可用、不可恢复。在 jdk11 及之后的版本中,同样的场景,会触发 GC 先回收掉垃圾,再创建对象。不会出现 oom。
  2. 缩堆不够高效。在 jdk8/11 中,缩堆只能通过 full gc 来回收,但是 full gc 停顿的时间也是相当长的,像我的简单业务都有 200ms 的停顿。因此缩堆要尽量避免 full gc,那么就要考虑支持 periodic gc 的版本了(periodic gc 只是 gc 的触发源,具体触发哪种 gc 由 g1 决定,但是比 full gc 要更高效的)。
发布:2025-08-10 01:13:14
修改:2025-09-25 01:44:37
链接:https://meethigher.top/blog/2025/jvm-mem/
标签:java 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏