為啥Java應用遷移到容器后會出現OOM?
JVM啟動后默認將最大使用堆大小設置為物理內存的四分之一,譬如一臺普通的x86服務器配置128G內存,那么啟動在容器的內啟動JVM會將自己最大允許使用的堆內存調整為32G內存,如果容器啟動時設置JVM只允許使用4G大小的內存,那么當JVM使用內存超過4G后,將會導致內核殺死JVM。測試代碼如下:
- import JAVA.util.ArrayList;
- import JAVA.util.List;
- public class MemEat {
- public static void main(String[] args) {
- List l = new ArrayList<>();
- while (true) {
- byte b[] = new byte[1048576];
- l.add(b);
- Runtime rt = Runtime.getRuntime();
- System.out.println( "free memory: " + rt.freeMemory() );
- }
- }
- }
代碼非常簡單,只是通過一個死循環不停地申請內存,如果是在JAVA 8u111版本之前,直接通過docker run -m 100m限制使用100M內存的情況下,運行一段時間后直接被內核殺死。輸出如下:
- # JAVA MemEat
- . . .
- free memory: 1307309488
- free memory: 1306260896
- free memory: 1305212304
- free memory: 1304163712
- free memory: 1303115120
- Killed
為了避免這種情況,可以通過“ -Xmx ”設置最大堆內存后再次運行。
- # JAVA -Xmx100m MemEat
- . . .
- free memory: 8382264
- free memory: 7333672
- free memory: 6285080
- free memory: 5236488
- Exception in thread "main" JAVA.lang.OutOfMemoryError: JAVA heap space MemEat.main(MemEat.JAVA:8)
可以看到JVM由于堆內存不足,自己退出了。這種在JVM添加參數的方式有個弊端:如果修改了容器的內存限制,還需要調整啟動參數。為此在JAVA 8u144版本之后添加了動態調整的功能,能夠根據用戶設定的內存限制動態調整,啟動參數如下:
- # JAVA -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap MemEat
當我們修改了內存參數后JVM便可以隨之調整。JAVA對于容器的支持不斷增強到最新的JAVA 10版本后,已經原生支持容器環境,無需添加任何參數。不僅如此,新版JAVA 10還支持CPU在容器內動態調整。如下所示JVM調整內存最大堆:
- # docker run -it -m 1024M --entrypoint bash openjdk:11-jdk
- # java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
- size_t MaxHeapSize = 268435456
可以看到上面的最大堆調整到內存限制的四分之一,而非物理內存的四分之一。還可以支持CPU自適應,如下所示:
- # docker run -it --CPUs 2 ---entrypoint bash openjdk:11-jdk
- jshell> Runtime.getRuntime().availableProcessors()
- $1 ==> 2
可以看到通過JAVA的API成功地獲取到當前設置的CPU個數。
如果是其他編程語言希望獲取到容器的CPU和內存限制,可以通過容器內的cgroup文件系統,如獲取容器內存的限制:
- # cat /sys/fs/cgroup/memory/memory.limit_in_bytes
- 104857600
【編輯推薦】