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,下面是其中的一部分:  
  
  
  
  
欢迎关注我的微信公众号:   coderbee笔记,可以更及时回复你的讨论。  
  