DEV Community

Truman
Truman

Posted on

JVM内存使用超出堆内存限制的问题排查

启动命令

-Xms8G -Xmx8G -Xmn4G -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParallelGC

Kubernetes监控中的内存使用情况
内存使用

根据启动命令的配置和 Kubernetes 显示的内存占用,出现了 JVM 内存(堆和非堆内存)与实际进程内存占用不一致 的现象。

JVM 配置

堆内存

  • 最小堆大小(-Xms):8GB
  • 最大堆大小(-Xmx):8GB
  • 年轻代大小(-Xmn):4GB (年轻代是堆的一部分)

元空间(Metaspace)

  • 初始元空间大小(-XX:MetaspaceSize):256MB
  • 最大元空间大小(-XX:MaxMetaspaceSize):256MB

垃圾回收器

  • GC 类型:Parallel GC (吞吐量优先,但会占用更多内存来优化 GC 性能)

Kubernetes 监控显示

  • 应用占用 9.4GB 内存,超出配置的 8GB 堆大小。

内存占用组成

JVM 应用的实际内存占用不仅包括堆内存,还包括以下部分:

1. 堆内存(Heap Memory)

  • 定义为 -Xms-Xmx8GB 堆内存
  • 组成部分:
    • 年轻代:配置为 -Xmn=4G
    • 老年代:存放从年轻代晋升的长生命周期对象。

2. 非堆内存(Non-Heap Memory)

  • 元空间(Metaspace): 配置了最大 256MB (-XX:MaxMetaspaceSize=256MB),但通常会稍微超过此值。
  • 代码缓存区(Code Cache): 存储 JIT 编译后的代码,大小随 JIT 编译器的使用而增长。
  • 线程栈内存(Thread Stack Memory)
    • 每个线程分配的栈内存由 -Xss 配置(默认 1MB)。
    • 线程数量较多时,线程栈内存可能占用大量内存。

3. 堆外内存(Direct Memory)

  • 使用 ByteBuffer.allocateDirect 或 NIO 时分配的直接内存。
  • 默认情况下,直接内存大小与最大堆大小(-Xmx)相同,可通过 -XX:MaxDirectMemorySize 限制。

4. GC 线程和内部内存

  • GC 线程: Parallel GC 垃圾回收线程会占用本地内存和线程栈空间。
  • 工作缓冲区: Parallel GC 会分配额外的工作缓冲区,用于垃圾回收操作,占用额外内存。

5. C 库或第三方库的内存

  • JNI 调用的本地库: 通过 JNI 调用的库(如 Netty、OpenSSL 等)可能分配堆外内存。
  • 这些内存由本地代码管理,不受 JVM 的直接控制。

可能原因分析

根据当前情况,以下是造成 Kubernetes 显示内存占用高于 JVM 配置的主要原因:

1. 线程栈内存消耗

  • 默认线程栈大小:1MB(通过 -Xss 配置)。
  • 影响
    • 如果应用使用大量线程(如处理高并发请求的线程池),线程栈内存会显著增加内存占用。
    • 假设线程数量为 1000,线程栈内存占用约为: 1MB * 1000 = 1GB

2. 堆外内存(Direct Memory)

  • 堆外内存的分配
    • 默认与最大堆大小一致(8GB)。
    • 通常由 NIO 或框架(如 Netty)分配。
  • 影响
    • 操作系统可能为堆外内存保留虚拟地址空间,即使实际使用未达到上限。

3. Parallel GC 的额外内存需求

  • GC 内存使用
    • Parallel GC 的多线程操作会分配额外内存用于内部数据结构(如标记、复制和整理)。
  • 相关配置
    • GC 线程数与 CPU 核心数相关,通过 ParallelGCThreads 调整。
  • 影响
    • 如果垃圾回收线程过多,内存占用会显著增加。

4. 元空间和代码缓存区增长

  • 元空间的增长
    • 虽然配置了最大 256MB (-XX:MaxMetaspaceSize=256MB),但 JVM 会动态分配更多内存用于类加载器和其他用途。
  • 代码缓存区
    • 用于存储 JIT 编译后的代码,随着运行时间增加可能增长。
  • 影响
    • 元空间和代码缓存区的增长可能导致非堆内存膨胀,进一步提升内存占用。

排查方法

以下方法可以帮助确认内存使用的具体来源:

1. 使用 Native Memory Tracking (NMT)

功能

NMT 可以精确追踪 JVM 内存分配,帮助识别内存的分布和使用情况。

启用 NMT

在 JVM 启动参数中添加以下配置:

-XX:NativeMemoryTracking=summary

查看内存分布

进入容器并运行:

jcmd 1 VM.native_memory summary

输出示例:

sh-4.4# jcmd 1 VM.native_memory summary
1:

Native Memory Tracking:

(Omitting categories weighting less than 1KB)

Total: reserved=9488872KB, committed=9037604KB
       malloc: 90692KB #530181
       mmap:   reserved=9398180KB, committed=8946912KB

-                 Java Heap (reserved=8388608KB, committed=8388608KB)
                            (mmap: reserved=8388608KB, committed=8388608KB)

-                     Class (reserved=216083KB, committed=19475KB)
                            (classes #24831)
                            (  instance classes #23390, array classes #1441)
                            (malloc=3091KB #81714)
                            (mmap: reserved=212992KB, committed=16384KB)
                            (  Metadata:   )
                            (    reserved=131072KB, committed=126208KB)
                            (    used=125492KB)
                            (    waste=716KB =0.57%)
                            (  Class space:)
                            (    reserved=212992KB, committed=16384KB)
                            (    used=15382KB)
                            (    waste=1002KB =6.12%)

-                    Thread (reserved=97958KB, committed=10654KB)
                            (thread #96)
                            (stack: reserved=97644KB, committed=10340KB)
                            (malloc=205KB #579)
                            (arena=109KB #188)

-                      Code (reserved=254760KB, committed=95700KB)
                            (malloc=7072KB #22671)
                            (mmap: reserved=247688KB, committed=88628KB)

-                        GC (reserved=310290KB, committed=310286KB)
                            (malloc=6542KB #99)
                            (mmap: reserved=303748KB, committed=303744KB)

-                  Compiler (reserved=821KB, committed=821KB)
                            (malloc=657KB #1513)
                            (arena=164KB #4)

-                  Internal (reserved=1608KB, committed=1608KB)
                            (malloc=1572KB #53569)
                            (mmap: reserved=36KB, committed=36KB)

-                     Other (reserved=16890KB, committed=16890KB)
                            (malloc=16890KB #52)

-                    Symbol (reserved=41301KB, committed=41301KB)
                            (malloc=36179KB #334650)
                            (arena=5122KB #1)

-    Native Memory Tracking (reserved=8483KB, committed=8483KB)
                            (malloc=199KB #3578)
                            (tracking overhead=8284KB)

-        Shared class space (reserved=16384KB, committed=12956KB, readonly=0KB)
                            (mmap: reserved=16384KB, committed=12956KB)

-               Arena Chunk (reserved=692KB, committed=692KB)
                            (malloc=692KB #373)

-                    Module (reserved=252KB, committed=252KB)
                            (malloc=252KB #4553)

-                 Safepoint (reserved=8KB, committed=8KB)
                            (mmap: reserved=8KB, committed=8KB)

-           Synchronization (reserved=2592KB, committed=2592KB)
                            (malloc=2592KB #25509)

-            Serviceability (reserved=18KB, committed=18KB)
                            (malloc=18KB #36)

-                 Metaspace (reserved=132121KB, committed=127257KB)
                            (malloc=1049KB #1245)
                            (mmap: reserved=131072KB, committed=126208KB)

-      String Deduplication (reserved=1KB, committed=1KB)
                            (malloc=1KB #8)

-           Object Monitors (reserved=4KB, committed=4KB)
                            (malloc=4KB #19)
Enter fullscreen mode Exit fullscreen mode

重点关注:

  • Thread:线程栈内存。
  • Direct:堆外内存。

2. 检查线程数量

进入 Pod 后,运行以下命令检查线程数:
ls /proc/<PID>/task | wc -l

输出示例

sh-4.4# ls /proc/1/task | wc -l
116
Enter fullscreen mode Exit fullscreen mode

如果线程数量较多(数百甚至上千),需要检查线程池配置,避免过多线程占用栈内存。

3. 检查 Direct Memory

可以通过以下代码查看直接内存的使用情况:

System.out.println("Max Direct Memory: " + sun.misc.VM.maxDirectMemory() / (1024 * 1024) + " MB");
Enter fullscreen mode Exit fullscreen mode

或者使用 jcmd 查看堆外内存分布。

4. 启用 GC 日志

在 JVM 启动参数中添加:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc.log

分析垃圾回收日志,确认是否有过多的垃圾回收线程或堆外内存清理未及时释放。

优化建议

1. 减少线程栈内存

设置更小的线程栈大小(例如 512KB):
-Xss512k

这可以显著减少线程栈内存占用。

2. 限制直接内存

设置直接内存的上限,例如 256MB:
-XX:MaxDirectMemorySize=256m

Top comments (0)