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。