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。
上面的配置运行一段时间后发现容器自动重启了,在 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 那么大的,多出来的其实是堆外内存。
JVM 的堆外内存主要包括:
冰山对象:冰山对象是指在 JVM 堆里占用的内存很小,但其实引用了一块很大的本地内存。DirectByteBuffer 和 线程都属于这类对象。
堆外内存泄漏一般很难通过 MAT 之类的工具来分析,必须通过操作系统层面的工具来。
关于本地内存的可以参考 IBM 的一个分享 《Where Does All The Native Memory Go》,可以网上找下这个 PPT,下面是其中的一部分:
欢迎关注我的微信公众号: coderbee笔记 ,可以更及时回复你的讨论。