众所周知,当我们执行没有任何调优参数(如
java -jar myapplication.jar
)的 Java 应用程序时,JVM 会自动调整几个参数,以便在执行环境中具有最佳性能。但是许多开发者发现,如果让 JVM ergonomics (即JVM人体工程学,用于自动选择和行为调整)对垃圾收集器、堆大小和运行编译器使用默认设置值,运行在Linux容器(docker,rkt,runC,lxcfs 等)中的 Java 进程会与我们的预期表现严重不符。
本篇文章采用简单的方法来向开发人员展示在 Linux 容器中打包 Java 应用程序时应该知道什么。
存在的问题
我们往往把容器当虚拟机,让它定义一些虚拟 CPU 和虚拟内存。其实容器更像是一种隔离机制:它可以让一个进程中的资源(CPU,内存,文件系统,网络等)与另一个进程中的资源完全隔离。Linux 内核中的 cgroups 功能用于实现这种隔离。
然而,一些从执行环境收集信息的应用程序已经在 cgroups 存在之前就被执行了。“top”,“free”,“ps”,甚至 JVM 等工具都没有针对在容器内执行高度受限的 Linux 进程进行优化。
场景还原
实验采用的是本人编写的测试代码。代码很简单,就是不断的创建byte数组并保存到list里面。代码所在项目:
blademainer/java-memory-demo
运行步骤:
- 首先使用docker运行java的测试镜像,并限制其最大可用的内存为
64M
1
docker run --name java-memory-demo --memory-swap=0 --memory-swappiness=0 -m 64m -e JAVA_OPTIONS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/jvmdump/" -v `pwd`/jvmdump:/jvmdump -d blademainer/java-memory-demo
JAVA_OPTIONS=”-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/jvmdump/“ 的作用是:在JVM报oom时dump出堆信息。
- 另起一个终端,监听镜像的事件:
1
docker events -f image=blademainer/java-memory-demo
运行一段时间后,java容器会被杀死,运行日志如下:
1 | ------------------------------------------ |
原因分析
事件分析
第二步docker events -f image=blademainer/java-memory-demo
事件监听输出如下:
1 | 2017-04-29T23:01:24.753731857+08:00 container create 92f98a1773549572cf8c3435350a6d1a885196884e957b35b5e1fa572e617a3b (image=blademainer/java-memory-demo, name=java-memory-demo) |
输出内容的第四行有个致命的报错container oom
导致java程序直接退出。或者使用docker inspect java-memory-demo
也能看到错误信息以及状态:
1 | "State": { |
docker直接杀掉了java容器,此时的退出前的java程序不会报任何错误信息也不会打印错误堆栈、调用shutdownHook等。jvmdump
也不会有dump的文件输出:
原因分析
按道理JVM会自动根据当前系统的可用内存来自动分配JVM的内存大小,那么JVM分配的内存应该不大于64M
,然而我们的java程序输出日志如下:
1 | MaxMemory: 3.46G |
MaxMemory: 3.46G
代表最大可用内存为3.46G
,明显不是我们期望的结果。JVM之所以不知道
他所在的环境是被限制了内存大小的,是因为docker采用cgroup技术来限制资源的,而JVM无法感知该限制,导致JVM根据宿主机器的最大内存来分配可用内存。
解决方案
使用启动参数来限制容器内JVM的内存
1 | docker run --name java-memory-demo --memory-swap=0 --memory-swappiness=0 -m 256m -e JAVA_OPTIONS="-Xmx128m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/jvmdump/" -v `pwd`/jvmdump:/jvmdump -d blademainer/java-memory-demo |
JAVA_OPTIONS
增加了-Xmx128m
JVM正确的打印了异常日志日志、调用了ShutdownHook以及正确的输出了HeapDumpPath
使用可以识别cgroup
限制的JVM
Tomcat容器的运行
运行时使用JAVA_OPTS
环境变量:
1 | docker run --memory-swap=0 --memory-swappiness=0 -m 256m -e JAVA_OPTS="-Xmx128m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/jvmdump/" -v `pwd`/jvmdump:/jvmdump tomcat |