容器内 JAVA 应用内存溢出问题分析
Java和Docker不是天然的朋友(Java诞生比Docker早得多)。 Docker可以设置内存和CPU限制,而Java不能自动检测到。使用Java的Xmx标识(繁琐/重复,仍然会多用内存)或新的实验性JVM标识,我们可以解决这个问题。
1. 自动设置(推荐):如果你想要的是,不显式的指定-Xmx,让Java进程自动的发现容器限制。
1.1 如果你想要的是jvm进程在容器中安全稳定的运行,不被容器kiil,并且你的JDK版本小于10(大于等于JDK10的版本不需要设置),你只需要额外设置以下JVM参数:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
即可保证你的Java进程不会因为内存问题被容器Kill。当然这个方式使用起来简单,可靠,缺点也很明显,资源利用率过低。
1.2 如果想在此基础上还想提高一些内存资源利用率,并且容器内存为1GB–4GB,我建议你设置-XX:MaxRAMFraction=2,在大于8G的可以尝试设置-XX:MaxRAMFraction=1。
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2 # 容器内存为:1GB–4GB
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 # 容器内存为:8GB+
前两个参数告诉JVM,你身在容器内部,即容器感知。MaxRAMFraction控制最大堆内存占容器内存的比例,即容器内存/MaxRAMFraction,只能取整数。oracle把这些参数支持backport到jdk1.8_171。不过,如果MaxRAMFraction取1,JVM Xmx接近容器最大内存,很容易被oom killed;而如果取2或者更大,则xmx又太小,或者容器内存要给很大才能让xmx满足需求,但是这样会浪费物理内存。在jdk1.8_191以上版本加入了百分比参数MaxRAMPercentage,可以精确控制。例如:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMPercentage=75.0 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/ # -XX:MaxRAMPercentage的值必须用double型,默认值为25.0。
2. 手动设置:如果你想要的是手动设置的体验,更加进一步的利用内存资源,那么你可能需要回到手动配置-Xmx。
2.1 上面的我们说到了自动配置,用起来很简单很舒服,自动发现容器限制,无需担心和思考去配置-Xmx。
2.2 下面是一些建议的配置
- 1G建议配置-Xmx750M;
- 2G建议配置-Xmx1700M;
- 4G建议配置-Xmx3500-3700M;
- 8G建议设置-Xmx7500-7600M;
总之就是至少保留300M以上的内存留给JVM的其他内存。如果堆特别大,可以预留到1G甚至2G。
2.3 手动设置用起来就没有那么舒服了,而且仍然会多用内存,当然资源利用率相对而言就更高了。
3. 其他新特性
Java 10 引入了 +UseContainerSupport(默认情况下启用),通过这个特性,可以使得JVM在容器环境分配合理的堆内存。 并且,在JDK8U191版本之后,这个功能引入到了JDK 8,而JDK 8是广为使用的JDK版本。
- UseContainerSupport
-XX:+UseContainerSupport允许JVM从主机读取cgroup限制,例如可用的CPU和RAM,并进行相应的配置。这样当容器超过内存限制时,会抛出OOM异常,而不是杀死容器。
该特性在Java 8u191 +,10及更高版本上可用。
注意,在191版本后,-XX:{Min|Max}RAMFraction 被弃用,引入了-XX:MaxRAMPercentage,其值介于0.0到100.0之间,默认值为25.0。
- 最佳实践
在应用的启动参数,设置 -XX:+UseContainerSupport,设置 -XX:MaxRAMPercentage=75.0,这样为其他进程(debug、监控)留下足够的内存空间,又不会太浪费RAM。
4. 总结
容器oom killed,跟一般传统的java.lang.OutOfMemoryError异常是两码事。java.lang.OutOfMemoryError发生是因为堆内存不够,此时需要增加Xmx。而容器oom killed,是因为堆外内存+堆内存总体超出限制而导致,是容器行为,所以不会产生heapdump。