Java进程RES内存异常增长问题排查求助
Java 17 Spring Boot 3 容器内存OOM排查问题
环境与背景
- 应用从Glassfish/Java 8迁移至Java 17 Spring Boot 3,运行在Kubernetes环境
- 容器内存请求与限制均为5.5GiB,JVM堆配置
-Xms=3.5GiB -Xmx=3.5GiB - 容器长期运行,通过Quartz调度执行任务
- 最终因容器内存触达限制,containerd触发OOM杀死Java进程
已做排查与观测
- jconsole监控显示GC稳定控制堆内存在3.5GiB以内,GC工作正常
- 启用Native Memory Tracker(NMT)未发现异常大小的非堆内存池
- 针对「堆密集型」短任务的内存变化对比:
- 任务前:NMT总提交4.535GiB(4755595KB),容器内
top显示RES为3.1GiB - 任务后:NMT总提交仅增加22493KB至4.56GiB,但
top显示RES增至4.9GiB且不再回落
- 任务前:NMT总提交4.535GiB(4755595KB),容器内
- jconsole显示任务执行期间堆内存始终在
-Xmx范围内,任务结束后堆内存恢复正常
NMT操作与输出
生成预运行基线:
[root@host-1 opt]# sudo -u appuser /opt/java/openjdk/bin/jcmd 191 VM.native_memory baseline 191: Baseline succeeded
任务后生成差异报告:
[root@host-1 opt]# sudo -u appuser /opt/java/openjdk/bin/jcmd 191 VM.native_memory summary.diff 191: Native Memory Tracking: (Omitting categories weighting less than 1KB) Total: reserved=5558072KB +11469KB, committed=4778088KB +22493KB - Java Heap (reserved=3670016KB, committed=3670016KB) (mmap: reserved=3670016KB, committed=3670016KB) - Class (reserved=331083KB +223KB, committed=26571KB +543KB) (classes #33968 +545) ( instance classes #31813 +515, array classes #2155 +30) (malloc=3403KB +223KB #83118 +5582) (mmap: reserved=327680KB, committed=23168KB +320KB) : ( Metadata) ( reserved=196608KB, committed=161088KB +4736KB) ( used=160529KB +4619KB) ( waste=559KB =0.35% +117KB) : ( Class space) ( reserved=327680KB, committed=23168KB +320KB) ( used=22637KB +321KB) ( waste=531KB =2.29% -1KB) - Thread (reserved=192733KB +9279KB, committed=20945KB +995KB) (thread #0) (stack: reserved=192188KB +9252KB, committed=20400KB +968KB) (malloc=327KB +16KB #1128 +54) (arena=218KB +11 #373 +18) - Code (reserved=254471KB +1005KB, committed=105347KB +15257KB) (malloc=6787KB +1005KB #31852 +3340) (mmap: reserved=247684KB, committed=98560KB +14252KB) - GC (reserved=243202KB +494KB, committed=128490KB +494KB) (malloc=13346KB +494KB #39503 +4498) (mmap: reserved=229856KB, committed=115144KB) - Compiler (reserved=1237KB +48KB, committed=1237KB +48KB) (malloc=1072KB +48KB #3239 +180) (arena=165KB #5) - Internal (reserved=7246KB +271KB, committed=7246KB +271KB) (malloc=7210KB +271KB #38329 +4138) (mmap: reserved=36KB, committed=36KB) - Other (reserved=587124KB +186KB, committed=587124KB +186KB) (malloc=587124KB +186KB #99 +6) - Symbol (reserved=38580KB +343KB, committed=38580KB +343KB) (malloc=36334KB +279KB #888476 +9518) (arena=2246KB +64 #1) - Native Memory Tracking (reserved=17131KB +436KB, committed=17131KB +436KB) (malloc=38KB +1KB #573 +12) (tracking overhead=17093KB +435KB) - Shared class space (reserved=16384KB, committed=12056KB) (mmap: reserved=16384KB, committed=12056KB) - Arena Chunk (reserved=479KB -991KB, committed=479KB -991KB) (malloc=479KB -991KB) - Tracing (reserved=32KB, committed=32KB) (arena=32KB #1) - Module (reserved=792KB +93KB, committed=792KB +93KB) (malloc=792KB +93KB #4842 +285) - Safepoint (reserved=8KB, committed=8KB) (mmap: reserved=8KB, committed=8KB) - Synchronization (reserved=150KB +11KB, committed=150KB +11KB) (malloc=150KB +11KB #1575 +105) - Serviceability (reserved=1KB, committed=1KB) (malloc=1KB #12) - Metaspace (reserved=197375KB +59KB, committed=161855KB +4795KB) (malloc=767KB +59KB #558 +81) (mmap: reserved=196608KB, committed=161088KB +4736KB) - String Deduplication (reserved=1KB, committed=1KB) (malloc=1KB #8) - Object Monitors (reserved=29KB +13KB, committed=29KB +13KB) (malloc=29KB +13KB #141 +66) [root@host-1 opt]#
RES(Resident Memory Size,单位KiB):虚拟内存空间(VIRT)的子集,表示任务当前使用的未交换物理内存。
原本预期RES应与NMT总提交值接近,但实际偏差巨大,相关资料未找到解决方案。
疑问
- 为何
top的RES值(4.9GiB)与Java NMT总提交值(3.5GiB)存在巨大差异? - 对NMT/top/jconsole的输出解读是否正确?
- 既然不是JVM堆问题,NMT也未显示原生内存异常,应使用哪些工具排查RES过高的根源?
- 其他排查建议?
解答
1. RES与NMT总提交值差异的原因
NMT仅追踪JVM自身管理的内存,不包含以下几类会被计入RES的内存:
- JVM进程的共享库/系统库内存:比如
libc.so、libjvm.so等系统或JVM依赖的共享库,这些库的内存会被多个进程共享,但top会将其全部计入单个进程的RES(实际物理内存是共享的,但统计时会重复计算)。 - 直接内存的未追踪部分:虽然NMT会追踪
Internal分类下的直接内存,但如果代码使用了非JVM API分配的原生内存(比如JNI调用C代码分配的内存,未通过ByteBuffer.allocateDirect),NMT无法统计。 - 容器层面的内存开销:比如容器运行时的辅助进程、文件系统缓存(如果应用有大量文件IO,页缓存会被计入容器的RES)。
- JVM的内存映射文件:比如读取的JAR包、配置文件等,通过
mmap加载到内存后,会被计入RES但可能未被NMT完全统计。
另外,Java 17的ZGC/G1GC在内存管理上与Java 8有差异,部分内存释放后可能未立即归还给操作系统(即内存压缩/保留策略),导致RES仍保持高位,但NMT显示已释放。
2. 输出解读的正确性
- jconsole:解读正确,堆内存稳定在
-Xmx内,GC正常说明堆层面无泄漏。 - NMT:总提交值的计算是正确的,但要注意NMT的
Other分类包含一些模糊的内存项,可能有未明确归类的内存,但从你的报告看Other仅增加186KB,无异常。 - top的RES:解读正确,但要注意RES包含共享内存和页缓存,不能直接等同于JVM实际使用的独占内存。任务前RES为3.1GiB低于NMT总提交,可能是因为部分JVM内存被swap出去,或共享库未完全加载;任务后RES升高,可能是任务触发了大量文件IO导致页缓存增加,或JNI内存泄漏。
3. 排查RES过高的工具
pmap:查看进程的内存映射详情,执行pmap -x <pid>,重点关注anon(匿名内存)和file(文件映射)部分,找占用过大的内存区域。perf:追踪内存分配,执行perf record -g -p <pid>,任务结束后perf report查看内存分配的调用栈,找出异常的原生内存分配。jmap -histo:live <pid>:虽然堆内存正常,但可以确认是否有未被GC回收的大对象,或类加载器泄漏(比如Quartz任务使用的类未被卸载)。- 容器层面的工具:使用
kubectl exec <pod> -- cat /proc/<pid>/smaps,查看每个内存区域的详细统计(包括共享内存的实际占用),区分独占和共享内存。 strace:追踪进程的mmap/munmap/malloc/free系统调用,看是否有大量未释放的内存分配。
4. 其他排查建议
- 检查Quartz任务的资源释放:任务执行时是否使用了JNI库、第三方原生依赖(比如图像处理、数据库驱动的原生部分),这些代码可能存在内存泄漏。
- 调整JVM参数:尝试添加
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -XX:ZCollectionInterval=60(如果用ZGC),或-XX:+ExplicitGCInvokesConcurrent,让JVM更积极地释放内存给操作系统。 - 检查容器的页缓存:执行
kubectl exec <pod> -- sync && echo 3 > /proc/sys/vm/drop_caches(仅测试用),如果RES下降,说明是页缓存导致的,可通过调整应用的文件IO策略(比如减少大文件读取,或使用直接IO)优化。 - 对比Java 8与Java 17的内存差异:在相同环境下运行Java 8版本的应用,观测RES变化,确认是否是Java 17或Spring Boot 3的兼容性问题。
- 检查JVM的Native内存泄漏:使用
jcmd <pid> VM.native_memory detail生成详细报告,查看每个分类下的具体内存分配,重点关注Code和Thread分类(你的报告中Code提交增加了15257KB,可能是JIT编译的代码未被释放)。
内容的提问来源于stack exchange,提问作者skinnydan72
相关产品推荐
相关产品推荐

