記憶體管理

當原生映像檔執行時,它不是在 Java HotSpot VM 上執行,而是在 GraalVM 提供的執行時期系統上執行。該執行時期包含所有必要的組件,其中一個就是記憶體管理。

原生映像檔在執行時配置的 Java 物件位於稱為「Java 堆積」的區域中。Java 堆積會在原生映像檔啟動時建立,並且可以在原生映像檔執行時增加或減少大小。當堆積滿載時,會觸發垃圾回收以回收不再使用的物件的記憶體。

為了管理 Java 堆積,Native Image 提供了不同的垃圾回收器 (GC) 實作

  • Serial GC 是 GraalVM Native Image 中的預設 GC。它針對低記憶體佔用量和小的 Java 堆積大小進行了最佳化。
  • G1 GC 是一個多執行緒 GC,經過最佳化以減少停止所有執行緒的暫停時間,從而提高延遲,同時實現高輸送量。要啟用它,請將選項 --gc=G1 傳遞給 native-image 建置器。目前,G1 垃圾回收器可以在 Linux AMD64 和 AArch64 架構上的 Native Image 中使用。(在 GraalVM 社群版中不可用。)
  • Epsilon GC(可在 GraalVM 21.2 或更新版本中使用)是一個不執行任何操作的垃圾回收器,它不執行任何垃圾回收,因此永遠不會釋放任何已配置的記憶體。此 GC 的主要使用案例是僅配置少量記憶體的短執行應用程式。要啟用 Epsilon GC,請在映像檔建置時指定選項 --gc=epsilon

效能考量 #

垃圾回收的主要指標是輸送量、延遲和佔用量

  • 輸送量 是在長時間內計算,未花在垃圾回收上的總時間的百分比。
  • 延遲 是應用程式的反應速度。垃圾回收暫停會對反應速度產生負面影響。
  • 佔用量 是處理程序的工作集,以頁面和快取行來衡量。

選擇 Java 堆積的設定總是在這些指標之間做出權衡。例如,非常大的年輕代可以最大化輸送量,但這樣做會犧牲佔用量和延遲。年輕代暫停可以透過使用小的年輕代來最小化,但會犧牲輸送量。

預設情況下,Native Image 會自動決定下方列出的 Java 堆積設定的值。確切的值可能取決於系統設定和使用的 GC。

  • 最大 Java 堆積大小 定義了整個 Java 堆積大小的上限。如果 Java 堆積滿載,且 GC 無法回收足夠的記憶體來配置 Java 物件,則配置將失敗並出現 OutOfMemoryError。請注意:最大堆積大小只是 Java 堆積的上限,而不是已消耗記憶體總量的上限,因為 Native Image 會將一些資料(例如執行緒堆疊、即時編譯程式碼(用於 Truffle 執行時期編譯)和內部資料結構)放置在與 Java 堆積分開的記憶體中。
  • 最小 Java 堆積大小 定義了 GC 始終可以假設為 Java 堆積保留的記憶體量,無論實際使用的記憶體量有多小。
  • 年輕代大小 決定了可以配置的 Java 記憶體量,而不會觸發垃圾回收。

串列垃圾回收器 #

Serial GC 針對低佔用量和小的 Java 堆積大小進行了最佳化。如果未指定其他 GC,則會隱式使用 Serial GC 作為 GraalVM 的預設值。也可以透過將選項 --gc=serial 傳遞給原生映像檔建置器來明確啟用 Serial GC。

# Build a native image that uses the serial GC with default settings
native-image --gc=serial HelloWorld

概觀 #

Serial GC 的核心是一個簡單的(非平行、非並行)停止和複製 GC。它將 Java 堆積分為年輕代和老年代。每個世代都由一組大小相等的區塊組成,每個區塊都是一個連續的虛擬記憶體範圍。這些區塊是 GC 內部用於記憶體配置和記憶體回收的單位。

年輕代包含最近建立的物件,並分為 edensurvivor 區域。新物件配置在 eden 區域中,當此區域滿載時,會觸發年輕代回收。在 eden 區域中存活的物件將被移動到 survivor 區域,而 survivor 區域中存活的物件會保留在該區域中,直到它們達到一定的年齡(已存活一定次數的回收),此時它們會被移動到老年代。當老年代滿載時,會觸發完整回收,以回收年輕代和老年代中未使用物件的空間。通常,年輕代回收比完整回收快得多,但執行完整回收對於保持低記憶體佔用量非常重要。預設情況下,Serial GC 會嘗試為各個世代找到一個能提供良好輸送量的大小,但當這樣做會減少收益時,則不會進一步增加大小。它還嘗試在年輕代回收和完整回收所花費的時間之間保持一個比例,以保持較小的佔用量。

如果未指定最大 Java 堆積大小,則使用 Serial GC 的原生映像檔會將其最大 Java 堆積大小設定為實體記憶體大小的 80%。例如,在具有 4GB RAM 的機器上,最大 Java 堆積大小將設定為 3.2GB。如果相同的映像檔在具有 32GB RAM 的機器上執行,則最大 Java 堆積大小將設定為 25.6GB。請注意,這只是最大值。根據應用程式的不同,實際使用的 Java 堆積記憶體量可能要低得多。若要覆寫此預設行為,請指定 -XX:MaximumHeapSizePercent 的值,或明確設定最大Java 堆積大小

請注意,GraalVM 版本(包括) 21.3 之前的版本,針對 Serial GC 使用不同的預設設定,沒有 survivor 區域、年輕代限制為 256 MB,以及在年輕代回收和老年代回收之間平衡時間的預設回收策略。可以使用以下方式啟用此設定: -H:InitialCollectionPolicy=BySpaceAndTime

請注意,GC 在執行垃圾回收時需要一些額外的記憶體(最糟的情況是最大堆積大小的 2 倍,通常會少得多)。因此,常駐集大小 RSS 在垃圾回收期間會暫時增加,這在任何具有記憶體限制的環境(例如容器)中都可能是一個問題。

效能調整 #

若要調整 GC 效能和記憶體佔用量,可以使用下列選項

  • -XX:MaximumHeapSizePercent - 如果未以其他方式指定最大 Java 堆積大小,則會將實體記憶體大小的百分比用作最大 Java 堆積大小。
  • -XX:MaximumYoungGenerationSizePercent - 年輕代的最大大小,以最大 Java 堆積大小的百分比表示。
  • -XX:±CollectYoungGenerationSeparately (自 GraalVM 21.0 起) - 決定完整 GC 是否單獨回收年輕代或與老年代一起回收。如果啟用,這可能會減少完整 GC 期間的記憶體佔用量。但是,完整 GC 可能需要更多時間。
  • -XX:MaxHeapFree (自 GraalVM 21.3 起) - 在回收後仍保留用於配置,因此不會傳回作業系統的可用記憶體區塊的最大總大小 (以位元組為單位)。
  • -H:AlignedHeapChunkSize (只能在映像檔建置時指定) - 堆積區塊的大小 (以位元組為單位)。
  • -H:MaxSurvivorSpaces (自 GraalVM 21.1 起,只能在映像檔建置時指定) - 用於年輕代的 survivor 空間的數量,也就是物件升級到老年代的最大年齡。當值為 0 時,在年輕代回收後存活的物件會直接升級到老年代。
  • -H:LargeArrayThreshold (只能在映像檔建置時指定) - 將陣列配置在自己的堆積區塊中,大小或以上的限制。被視為大型的陣列的配置成本較高,但它們永遠不會被 GC 複製,這可以減少 GC 額外負荷。
# Build and execute a native image that uses a maximum heap size of 25% of the physical memory
native-image --gc=serial -R:MaximumHeapSizePercent=25 HelloWorld
./helloworld

# Execute the native image from above but increase the maximum heap size to 75% of the physical memory
./helloworld -XX:MaximumHeapSizePercent=75

下列選項僅適用於 -H:InitialCollectionPolicy=BySpaceAndTime

  • -XX:PercentTimeInIncrementalCollection - 決定 GC 應該花多少時間進行年輕代回收。預設值為 50 時,GC 會嘗試平衡在年輕代和完整回收所花費的時間。增加此值會減少完整 GC 的數量,這可以提高效能,但可能會惡化記憶體佔用量。減少此值會增加完整 GC 的數量,這可以改善記憶體佔用量,但可能會降低效能。

G1 垃圾回收器 #

Oracle GraalVM 還提供了 Garbage-First (G1) 垃圾回收器,它是基於 Java HotSpot VM 中的 G1 GC。目前,G1 垃圾回收器可以在 Linux AMD64 和 AArch64 架構上的 Native Image 中使用。(在 GraalVM 社群版中不可用。)

要啟用它,請將選項 --gc=G1 傳遞給 native-image 建構器。

# Build a native image that uses the G1 GC with default settings
native-image --gc=G1 HelloWorld

注意:在 GraalVM 20.0、20.1 和 20.2 中,G1 GC 被稱為低延遲 GC,並且可以透過實驗性選項 -H:+UseLowLatencyGC 啟用。

概觀 #

G1 是一個分代的、增量的、並行的、大部分並行的、停止世界 (stop-the-world) 和疏散式的 GC。它的目標是在延遲和吞吐量之間提供最佳的平衡。

某些操作總是會在停止世界暫停中執行,以提高吞吐量。其他在應用程式停止時會花費更多時間的操作,例如全堆操作(如全域標記),則會與應用程式並行且同時執行。G1 GC 嘗試在較長的時間內以高機率達到設定的暫停時間目標。然而,對於給定的暫停時間,並沒有絕對的確定性。

G1 將堆分割成一組大小相等的堆區域,每個區域都是虛擬記憶體的連續範圍。區域是 GC 內部用於記憶體分配和記憶體回收的單位。在任何給定的時間,這些區域中的每一個都可以是空的,或者被分配給特定的世代。

如果沒有指定最大 Java 堆大小,則使用 G1 GC 的原生映像檔會將其最大 Java 堆大小設定為物理記憶體大小的 25%。例如,在具有 4GB RAM 的機器上,最大 Java 堆大小將設定為 1GB。如果在具有 32GB RAM 的機器上執行相同的映像檔,則最大 Java 堆大小將設定為 8GB。要覆寫此預設行為,請為 -XX:MaxRAMPercentage 指定一個值,或明確設定最大 Java 堆大小

效能調校 #

G1 GC 是一個自適應的垃圾收集器,其預設值使其能夠在不修改的情況下高效運作。但是,可以針對特定應用程式的效能需求進行調校。以下是在進行效能調校時可以指定的一小部分選項

  • -H:G1HeapRegionSize (只能在映像檔建構時指定) - G1 區域的大小。
  • -XX:MaxRAMPercentage - 如果沒有以其他方式指定最大堆大小,則用作最大堆大小的物理記憶體大小的百分比。
  • -XX:MaxGCPauseMillis - 最大暫停時間的目標。
  • -XX:ParallelGCThreads - 在垃圾收集暫停期間用於並行工作的最大執行緒數。
  • -XX:ConcGCThreads - 用於並行工作的最大執行緒數。
  • -XX:InitiatingHeapOccupancyPercent - 觸發標記週期的 Java 堆佔用率閾值。
  • -XX:G1HeapWastePercent - 集合候選中允許的未回收空間。如果集合候選中的可用空間低於該值,則 G1 會停止空間回收階段。
# Build and execute a native image that uses the G1 GC with a region size of 2MB and a maximum pause time goal of 100ms
native-image --gc=G1 -H:G1HeapRegionSize=2m -R:MaxGCPauseMillis=100 HelloWorld
./helloworld

# Execute the native image from above and override the maximum pause time goal
./helloworld -XX:MaxGCPauseMillis=50

記憶體管理選項 #

本節介紹與所使用的 GC 無關的最重要的記憶體管理命令列選項。對於所有數值,可以使用後綴 kmg 進行縮放。可以使用 native-image --expert-options-all 列出原生映像檔建構器的更多選項。

Java 堆大小 #

執行原生映像檔時,將根據系統配置和所使用的 GC 自動確定合適的 Java 堆設定。要覆寫此自動機制並在執行時明確設定堆大小,可以使用以下命令列選項

  • -Xmx - 最大堆大小 (以位元組為單位)
  • -Xms - 最小堆大小 (以位元組為單位)
  • -Xmn - 年輕代的大小 (以位元組為單位)

也可以在映像檔建構時預先設定預設的堆設定。然後,指定的值將在執行時用作預設值

  • -R:MaxHeapSize (自 GraalVM 20.0 起) - 最大堆大小 (以位元組為單位)
  • -R:MinHeapSize (自 GraalVM 20.0 起) - 最小堆大小 (以位元組為單位)
  • -R:MaxNewSize (自 GraalVM 20.0 起) - 年輕代的大小 (以位元組為單位)
# Build a native image with the default heap settings and override the heap settings at run time
native-image HelloWorld
./helloworld -Xms2m -Xmx10m -Xmn1m

# Build a native image and "bake" heap settings into the image. The specified values will be used at run time
native-image -R:MinHeapSize=2m -R:MaxHeapSize=10m -R:MaxNewSize=1m HelloWorld
./helloworld

壓縮參考 #

Oracle GraalVM 支援使用 32 位元而不是 64 位元的壓縮 Java 物件參考。預設情況下啟用壓縮參考,並且可以對記憶體佔用量產生很大影響。但是,它們將最大 Java 堆大小限制為 32 GB 的記憶體。如果需要超過 32 GB 的記憶體,則需要停用壓縮參考。

  • -H:±UseCompressedReferences (只能在映像檔建構時指定) - 決定是否使用 32 位元而不是 64 位元的 Java 物件參考。

原生記憶體 #

原生映像檔也可能會分配與 Java 堆分開的記憶體。一個常見的用例是直接引用原生記憶體的 java.nio.DirectByteBuffer

  • -XX:MaxDirectMemorySize - 直接緩衝區分配的最大大小。

列印垃圾收集 #

執行原生映像檔時,可以使用以下選項來列印有關垃圾收集的一些資訊。詳細列印哪些資料取決於所使用的 GC。

  • -XX:+PrintGC - 列印每次垃圾收集的基本資訊
  • -XX:+VerboseGC - 可以新增以列印更多垃圾收集詳細資訊
# Execute a native image and print basic garbage collection information
./helloworld -XX:+PrintGC

# Execute a native image and print detailed garbage collection information
./helloworld -XX:+PrintGC -XX:+VerboseGC

延伸閱讀 #

與我們聯繫