JVM 堆外内存泄漏分析(一)

1. JVM 感知容器资源

Java 应用部署在 Kubernetes 集群里,每个容器只运行一个进程, JVM 的启动命令是打包在镜像文件里的。

常规的方式是采用 -Xmx4g -Xms2g 这样的参数来指定 JVM 堆的最大、最小尺寸,如果需要调整堆大小就需要重新打包镜像。

为了避免因为修改堆大小而重新打包,从 JDK 8u191 版本开始支持 JVM 感知容器资源限制,这样在调整 JVM 内存分配时就不需要重新打包镜像文件,采用下面的参数来使 JVM 在启动时感知到容器的资源限制,并设定堆的大小:

-XX:+UseCGroupMemoryLimitForHeap
-XX:InitialRAMPercentage=60.00
-XX:MaxRAMPercentage=80.00
-XX:MinRAMPercentage=60.00 

假如分配给容器的内存上限是 4G,那么上述配置,JVM 堆的初始大小和最小尺寸是 4G * 0.6 即 2.4G,最大尺寸是 4G * 0.8 即 3.2G。

2. JVM 被 oomkill

上面的配置运行一段时间后发现容器自动重启了,在 linux 下通过 dmesg 命令查看系统日志,可以看到类似下面的日志:

Aug  8 15:32:40 H-LDOCKER-01 kernel: [ pid ]   uid   tgid   total_vm      rss        nr_ptes   nr_pmds   swapents   oom_score_adj  name
Aug  8 15:32:40 H-LDOCKER-01 kernel: [33775]   1001  33775  7624373       2036828    4476      32        0          -998           java
Aug  8 15:32:40 H-LDOCKER-01 kernel: Memory cgroup out of memory: Kill process 33775 (java) score 0 or sacrifice child
Aug  8 15:32:40 H-LDOCKER-01 kernel: Killed process 33775 (java) total-vm:30497492kB, anon-rss:8134056kB, file-rss:13256kB

注意:上面日志 rss 列表示进程占用的内存大小,对应的值是 2036828,单位是 4KB,也即这个 Java 进程占用了 7.77G,容器分配的内存上限是 8G。第3、4行表示 Java 进程被 oom_killer 了。

OOM_killer 是 Linux 的一种自我保护措施,当系统内存不足时为防止出现严重问题,系统唤醒 oom_killer,挑出 /proc/<pid>/oom_score 值最大的进程并 kill。

因为应用也输出了 GC 日志,从进程被 kill 前的那个时间节点的日志来看,JVM 的堆是远远没有 7G 那么大的,多出来的其实是堆外内存。

3. JVM 堆外内存

JVM 的堆外内存主要包括:

  • JVM 自身运行占用的空间;
  • 线程栈分配占用的系统内存;
  • DirectByteBuffer 占用的内存;
  • JNI 里分配的内存;
  • Java 8 开始的元数据空间;
  • NIO 缓存
  • Unsafe 调用分配的内存;
  • codecache

冰山对象:冰山对象是指在 JVM 堆里占用的内存很小,但其实引用了一块很大的本地内存。DirectByteBuffer 和 线程都属于这类对象。

堆外内存泄漏一般很难通过 MAT 之类的工具来分析,必须通过操作系统层面的工具来。

关于本地内存的可以参考 IBM 的一个分享 《Where Does All The Native Memory Go》,可以网上找下这个 PPT,下面是其中的一部分:
where-does-all-native-memory-go-2

where-does-all-native-memory-go-3

where-does-all-native-memory-go-5

where-does-all-native-memory-go-6


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据