Java进程内存占用远超分配值的原因排查(Spring Boot场景)
大内存Java进程内存占用超出预期问题
应用环境与启动参数
我们运行了一个配置约100GB堆内存的Spring Boot应用,所在节点总内存为186GB,进程会在可用内存耗尽后被系统杀死。启动参数如下:
/usr/java/1.8/bin/java -Xmx105104m -Xms105104m -XX:+UseParallelGC -XX:-OmitStackTraceInFastThrow -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:MaxGCPauseMillis=999 -XX:GCTimeRatio=19 -XX:NativeMemoryTracking=detail -XX:+PrintClassHistogram -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:GCLogFileSize=4M -XX:NumberOfGCLogFiles=5 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=logs -Dfile.encoding=UTF-8 -Dcom.sun.management.jmxremote.port=33333 -Dcom.sun.management.jmxremote.rmi.port=33333 -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -classpath /tmp/test.jar
JVM Native Memory Tracking(NMT)统计结果
通过jcmd获取的内存汇总显示,JVM总预留内存约为108GB:
/usr/java/1.8/bin/jcmd 39169 VM.native_memory summary
输出:
39169: Native Memory Tracking: Total: reserved=116072104KB, committed=116020952KB - Java Heap (reserved=107626496KB, committed=107626496KB) (mmap: reserved=107626496KB, committed=107626496KB) - Class (reserved=269229KB, committed=268973KB) (classes #37441) (malloc=43949KB #111442) (mmap: reserved=225280KB, committed=225024KB) - Thread (reserved=2185937KB, committed=2185937KB) (thread #2117) (stack: reserved=2175044KB, committed=2175044KB) (malloc=7277KB #12696) (arena=3616KB #4217) - Code (reserved=291291KB, committed=240399KB) (malloc=41691KB #52137) (mmap: reserved=249600KB, committed=198708KB) - GC (reserved=3971460KB, committed=3971460KB) (malloc=39360KB #2400) (mmap: reserved=3932100KB, committed=3932100KB) - Compiler (reserved=6110KB, committed=6110KB) (malloc=5965KB #9682) (arena=145KB #18) - Internal (reserved=1663307KB, committed=1663303KB) (malloc=1663271KB #81858) (mmap: reserved=36KB, committed=32KB) - Symbol (reserved=45400KB, committed=45400KB) (malloc=41748KB #429276) (arena=3652KB #1) - Native Memory Tracking (reserved=12387KB, committed=12387KB) (malloc=1150KB #15182) (tracking overhead=11237KB) - Arena Chunk (reserved=489KB, committed=489KB) (malloc=489KB)
/proc/smaps内存统计结果
通过系统smaps命令统计的实际内存占用远高于NMT结果,进程持续占用内存直至达到节点186GB上限后被杀死:
# 统计PSS内存 cat /proc/39169/smaps | grep -i pss | awk '{Total+=$2} END {print Total/1024/1024" GB"}' # 输出:133.006 GB # 统计私有脏页内存 cat /proc/39169/smaps | grep -i 'Private_Dirty' | awk '{Total+=$2} END {print Total/1024/1024" GB"}' # 输出:132.975 GB
核心疑问
Java进程是否应该占用超过分配的内存?
解答
Java进程的实际内存占用确实会超出-Xmx指定的堆内存上限,因为JVM除了Java堆之外,还有大量非堆内存开销(比如线程栈、元空间、Direct内存、GC相关内存等)。但你的场景中NMT统计(108GB)与smaps统计(133GB)差距过大,需要重点排查以下方向:
Native内存泄漏
- 检查应用是否使用大量JNI调用、第三方本地库(如Netty、自定义JNI扩展),这类内存分配可能未被NMT完全追踪,或存在泄漏。
- 排查Direct ByteBuffer使用:默认
-XX:MaxDirectMemorySize等于-Xmx,若大量分配Direct内存且未及时释放,会占用系统内存,部分场景下NMT可能将其归类到Internal区域未明确标记。
NMT统计局限性
- NMT无法统计所有系统级内存开销,比如内核为进程分配的页表内存、共享内存映射中的私有脏页、部分JNI库的内存分配等。
- 执行
jcmd <pid> VM.native_memory detail查看细粒度内存分配,确认是否有未被统计的大块内存区域。
线程栈与GC隐藏开销
- 虽然NMT统计的线程栈内存(约2.1GB)符合2117个线程×1MB栈的预期,但如果线程栈实际使用超过预留(如递归深度过大),会导致实际内存占用增加。
- ParallelGC在大堆场景下,可能需要额外内存用于GC标记、存活对象复制等临时操作,部分开销未被NMT实时统计。
系统统计差异
smaps的Private_Dirty统计的是进程实际修改的私有内存页,包含JVM堆、非堆、所有本地分配内存;而NMT仅追踪JVM内部管理的内存,两者差异通常来自未被JVM追踪的本地内存。
排查建议
- 显式设置
-XX:MaxDirectMemorySize限制Direct内存大小(如-XX:MaxDirectMemorySize=10G),观察内存变化。 - 使用
jmap -histo:live <pid>查看存活对象,确认堆内存使用是否符合预期,排查堆内内存泄漏。 - 用
pmap -x <pid>分析进程内存映射,找出占用大块内存的匿名映射或文件映射,定位内存来源。 - 通过
jstack <pid>检查线程数量与栈信息,确认是否存在线程泄漏或栈过大的情况。
内容的提问来源于stack exchange,提问作者R H
相关产品推荐
相关产品推荐

