You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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且不再回落
  • 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总提交值接近,但实际偏差巨大,相关资料未找到解决方案。

疑问

  1. 为何top的RES值(4.9GiB)与Java NMT总提交值(3.5GiB)存在巨大差异?
  2. 对NMT/top/jconsole的输出解读是否正确?
  3. 既然不是JVM堆问题,NMT也未显示原生内存异常,应使用哪些工具排查RES过高的根源?
  4. 其他排查建议?

解答

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 07:45:53