環節。這裡總結下我們專案裡的最佳實踐。
Java Heap 基礎知識預設情況下,JVM 自動分配的 heap 大小取決於機器配置。比如我們到一臺 64G 記憶體伺服器:
java -XX:+PrintFlagsFinal -version | grep -Ei "maxheapsize|maxram" uintx DefaultMaxRAMFraction = 4 {product} uintx MaxHeapSize := 16875782144 {product} uint64_t MaxRAM = 137438953472 {pd product} uintx MaxRAMFraction = 4 {product} double MaxRAMPercentage = 25.000000 {product}java version "1.8.0_192"Java(TM) SE Runtime Environment (build 1.8.0_192-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
可以看到,JVM 分配的最大 MaxHeapSize 為 16G,計算公式如下:
MaxHeapSize = MaxRAM * 1 / MaxRAMFraction
MaxRAMFraction 預設是4,意味著,每個 JVM 最多使用 25% 的機器記憶體。
但是需要注意的是,JVM 實際使用的記憶體會比 heap 記憶體大:
JVM記憶體 = heap 記憶體 + 執行緒stack記憶體 (XSS) * 執行緒數 + 啟動開銷(constant overhead)
預設的 XSS 通常在 256KB 到 1MB,也就是說每個執行緒會分配最少 256K 額外的記憶體, constant overhead 是 JVM 分配的其他記憶體。
我們可以透過 -Xmx 指定最大堆大小。
java -XX:+PrintFlagsFinal -Xmx1g -version | grep -Ei "maxheapsize|maxram" uintx DefaultMaxRAMFraction = 4 {product} uintx MaxHeapSize := 1073741824 {product} uint64_t MaxRAM = 137438953472 {pd product} uintx MaxRAMFraction = 4 {product} double MaxRAMPercentage = 25.000000 {product}java version "1.8.0_192"Java(TM) SE Runtime Environment (build 1.8.0_192-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
此外,還可以使用 XX:MaxRAM 來指定。
java -XX:+PrintFlagsFinal -XX:MaxRAM=1g -version | grep -Ei
但是指定 -Xmx 或者 MaxRAM 需要了解機器的記憶體,更好的方式是設定 MaxRAMFraction,以下是不同的 Fraction 對應的可用記憶體比例:
+----------------+-------------------+| MaxRAMFraction | % of RAM for heap ||----------------+-------------------|| 1 | 100% || 2 | 50% || 3 | 33% || 4 | 25% |+----------------+-------------------+
容器環境的 Java Heap
容器環境,由於 Java 獲取不到容器的記憶體限制,只能獲取到伺服器的配置:
$ docker run --rm alpine free -m total used free shared buffers cachedMem: 1998 1565 432 0 8 1244$ docker run --rm -m 256m alpine free -m total used free shared buffers cachedMem: 1998 1552 445 1 8 1244
這樣容易引起不必要問題,例如限制容器使用100M 記憶體,但是 JVM 根據伺服器配置來分配初始化記憶體,導致 Java 程序超過容器限制被kill掉。為了解決這個問題,可以設定 -Xmx 或者 MaxRAM 來解決,但就想第一部分描述的一樣,這樣太不優雅了!
為了解決這個問題,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。
最佳實踐拉取最新的 openjdk:8-jre-alpine 作為底包,截止這篇部落格,最新的版本是 212,>191。
docker run -it --rm openjdk:8-jre-alpine java -versionopenjdk version "1.8.0_212"OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
我們構建一個基礎映象,dockerfile 如下:
FROM openjdk:8-jre-alpineMAINTAINER jadepengRUN echo "http://mirrors.aliyun.com/alpine/v3.6/main" > /etc/apk/repositories \ && echo "http://mirrors.aliyun.com/alpine/v3.6/community" >> /etc/apk/repositories \ && apk update upgrade \ && apk add --no-cache procps unzip curl bash tzdata \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezoneRUN apk add --update ttf-dejavu && rm -rf /var/cache/apk/*
在應用的啟動引數,設定 -XX:+UseContainerSupport,設定 -XX:MaxRAMPercentage=75.0,這樣為其他程序(debug、監控)留下足夠的記憶體空間,又不會太浪費 RAM。