有了HotSpot JVM為什麼還需要OpenJ9?

騎牛上青山發表於2023-02-05

什麼是OpenJ9

OpenJ9是一個致力於構建更小記憶體使用,更快啟動速度和更高吞吐量的獨立實現的Java虛擬機器。專案由IBM發起,並在之後開源並捐贈給Eclipse基金會。

為什麼需要OpenJ9

HotSpot JVM在Java虛擬機器領域獨領風騷多年了,但是近年來有GraalVMOpenJ9等等後起之秀嶄露頭角,開始在各自的領域發力。

正如OpenJ9自己的介紹一樣:

A Java Virtual Machine for OpenJDK that's optimized for small footprint, fast start-up, and high throughput

OpenJ9的特點就是效能:低記憶體佔用,快速啟動,高吞吐。我們就來看看為了實現這些能力OpenJ9都做了什麼,然後回過頭再來看他是否能夠在某些場合替代HotSpot JVM

效能

從官網上擷取了官方對於OpenJ9的效能對比。可以看到無論是jdk11還是jdk8,OpenJ9在啟動時間和記憶體佔用上都佔有較大優勢。

類共享

OpenJ9的一大特點就是類共享。共享類無需使用者進行特殊處理,JVM會自行進行處理來最佳化記憶體佔用和改進啟動時間。在OpenJ9的實現中,所有的系統類,應用類和AOT預編譯的程式碼都能被存在共享記憶體的動態類快取中。類共享對於多個執行相同程式碼的JVM將是巨大的最佳化,因此在當前的雲原生的蓬勃發展下OpenJ9是一個非常有誘惑力的選擇。

類共享使用

想要開啟使用類共享很簡單,只要在JVM啟動項中新增-Xshareclasses[:name=<cachename>]即可,JVM會自行構建快取。

類共享原理

共享類快取

共享類快取(SCC, shared classes cache)是一個固定大小的共享記憶體區域。除非配置了不持久化,否則SCC資料即使在JVM重啟後也會依然存在。

OpenJ9的共享快取不屬於某個JVM,各個JVM之間也不會有主次之分,但是所有的JVM都能夠對共享快取進行讀寫。

類快取使用

一般的JVM在裝載類的時候遵循如下的流程:

使用類共享的情況下類的載入機制會發生變化:

啟用類共享的情況下,在父類載入器層層載入都沒法獲取類時會去共享快取查詢類,然後才會嘗試去檔案系統獲取。

java.net.URLClassLoader (在Java9+ jdk.internal.loader.BuiltinClassLoader)已經整合了共享類快取的API,因此所有繼承java.net.URLClassLoader的類載入器都能夠使用共享類快取。如果是自定義的類載入器,可以使用OpenJ9提供的API。

OpenJ9的實現中,Java類被分為了兩部分:

  • ROMClass 只讀,儲存的是類的不可變資料
  • RAMClass 可寫,儲存的是類的可變資料,例如靜態類變數

雖然RAMClass指向了ROMClass,但是這兩者是完全分開的。因此在不同的JVM之間分享ROMClass以及在同一個JVM使用RAMClass是很安全的。在未開啟類共享的情況下,當JVM載入類時,會分別生成RAMClassROMClass並儲存在本地的記憶體中。如果開啟了類共享,JVM載入類時發現共享記憶體中已經存在了該類,那麼就只需要建立RAMClass然後存放在本地記憶體使用即可。

AOT編譯後的程式碼也會被儲存在共享快取中。當啟用共享類快取時,AOT會將將Java類編譯成本機程式碼,以便同一程式後續使用。

檔案系統變化導致的類快取問題

因為共享快取是沒有過期時間的,因此可能會存在類檔案產生變動導致的快取失效。因此JVM需要處理這種情況下的類快取的更新問題。JVM需要保證類載入器獲取的類必須和檔案系統中的類是一致的。

JVM透過將時間戳值儲存到快取中並將快取值與實際值進行比較來檢測檔案系統更新。在類發生更新的情況下這些操作對於類載入是透明的,因此使用者對於類進行修改操作都很容易被感知到並且進行相應的處理。

快取版本差異

在某些情況下,從一個版本的JVM建立的快取可能與從不同版本建立的快取不相容。遇到這種情況即使兩個快取名稱相同,JVM也會依然建立一個新快取,同時透過共享類快取的世代號(generation number)來檢測衝突。

redefine和retransform類

類快取機制聽上去很合理,但是特殊情況下會有些不一樣,比如當你使用了Java Agent時,會有一些類會被redefined或者retransformed。針對這兩種情況,OpenJ9做了不同的處理:

  • redefined redefine會替換位元組碼,因此這種類不會被存放入快取中
  • retransformed retransform會修改位元組碼,並且有可能會進行多次的修改,這種類預設不會被存入快取,但是可以透過-Xshareclasses:cacheRetransformed選項來開啟

AOT

AOT透過將java類編譯成native code並快取到共享資料快取中。後續虛擬機器可以從共享資料快取載入和使用AOT的程式碼,而不會導致效能下降。

如果要關閉,可以使用-Xnoaot引數進行配置

記憶體管理

GC策略

OpenJ9提供了一系列GC的策略用於不同場合的記憶體管理。

gencon

gencon(Generational Concurrent GC)是OpenJ9預設的GC策略,使用-Xgcpolicy:gencon進行配置。這個GC策略適用於大多數的應用,尤其是有許多生命週期很短的物件的事務性應用。此策略旨在不影響吞吐量的情況下減少GC暫停次數。

此策略類似於HotSpot JVM的分代收集策略,只是OpenJ9會在一些細節上有一些不同。

gencon策略中,Java堆被分成了兩部分:

  • nursery 儲存新建立的物件
  • tenure 儲存達到tenure age的物件

nursery被分為了兩個部分:allocatesurvivor。GC過程如下圖所示:

  1. 新物件進入nurseryallocate區域
  2. allocate漸漸增長直至完全充滿
  3. 本地清掃程式啟動,將所有可達的物件放入到survivor,或者如果物件已經到達tenure age,則直接進入tenure區域
  4. 之後allocatesurvivor角色互換,先前的allocate變為survivor,先前的survivor則變為allocate,為下一次GC作準備

allocatesurvivor的相對大小會根據一種叫做tilting的動態調整技術來進行變化。剛開始allocatesurvivor的大小是五五開的,在清理過程中如果發現哪一邊所需的空間較小,會對空間進行動態調整以滿足GC的需求。以此可以儘可能減少GC的週期。

其中tenure age是指物件在allocatesurvivor的切換過程中存活下來的次數,JVM會依據此資料來決定物件是否轉移到tenure。可以透過-Xgc:scvTenureAge=<n>引數來設定初始的tenure age,後續的tenure age可能會隨著GC的程式由JVM進行自適應來最佳化當前的空間使用率。當然如果要關閉tenure age自適應,可以使用此引數-Xgc:scvNoAdaptiveTenure

tenure預設會被分為兩部分:小物件區域(SOA),大物件區域(LOA),SOA中存放不大於64KB的物件,LOA則相反。如果要禁用LOA,可以使用-Xnoloa引數。

balanced

balancedGC策略使用引數-Xgcpolicy:balanced啟用(需注意此策略僅支援64位平臺)。在此策略下Java堆被分為一個個不同的region(1024 - 2048),這些region由增量分代收集器單獨管理,以減少大堆上的最大暫停時間並提高垃圾回收的效率。此策略將堆進行切分以避免全域性的垃圾回收,以此來減少垃圾回收時的長暫停。

balanced策略類似於HotSpot中的G1收集器。

在虛擬機器啟動的時候,堆記憶體會被劃分為大小相等的region,這些region就是balanced gc策略的基本單元。

region存在如下特點:

  1. 由於region的特殊性,在一開始就強制限定了物件的最大大小。
  2. 物件始終被分配在單個region內,不會跨region分配。
  3. region大小始終是2的N次冪,且是在啟動時根據堆的最大值來決定的。
  4. 虛擬機器總是會生成1024~2048個region

基於上述特性我們來看下balanced gc策略的gc流程。

上圖是堆上的region的劃分。其中age為0的是edenage為24是old,中間的region則分佈著1-23的age

在進行垃圾回收時eden區總是會參與其中,而old只在少數情況下會被加入其中。當進行過一次垃圾回收後,age為N的倖存者會被放入到age為N+1的區域中。然後隨著時間的推移,可用的倖存區域會變得越來越少,之後到了某個時間節點就需要進行全域性標記清理整個堆。

大多數的物件可以很輕鬆的存放入region中,但是也有少部分的大物件沒法正常儲存在region中,因此提供了Arraylets來處理當前情況。

Arraylets

Arraylets是用來解決大物件無法在單個region中儲存的問題的。Arraylets會有一個結構Spine,其中存放著類指標和大小,其中還包含Arrayoids指向各個葉子結點。以此可以將大物件進行切分,儲存到不同的region中。

optavgpause

optavgpause(optimize for pause time)策略使用引數-Xgcpolicy:optavgpause來啟用。此策略可以減少GC暫停時間,但是會犧牲部分吞吐量。

optavgpause策略使用平面的Java堆。全域性GC進行迴圈併發mark-sweep標記清除操作。由於其全域性併發處理的特性,會顯著減少GC暫停時間,但是會大大影響吞吐量。

optthruput

optthruput(optimize for throughput)策略使用引數-Xgcpolicy:optthruput來啟用。此策略和optavgpause策略有著類似的設計,只是此策略專注於吞吐量的最佳化,因此雖然提升了吞吐量,但是會有較高的GC暫停時間。

optthruput策略使用平面的Java堆。全域性GC使用mark-sweep進行迴圈標記清除操作。由於不是併發清理,因此需要對堆進行獨佔訪問,導致應用程式執行緒在操作發生時停止。因此,可能會出現長時間的GC停頓。

metronome

metronome策略使用引數-Xgcpolicy:metronome來啟用,其只支援linux x86-64AIX平臺。此策略是一種具備較短暫停時間的增量的,確定的垃圾回收策略。

metronome策略會在堆上分配連續的範圍,將這些劃分為大小相等的區域,通常為64Kb。其中每個區域中只存放大小相等的物件或者是arraylet。這種形式簡化了物件分配和空間合併的,以此保證GC的吞吐量。

如何選擇合適的GC策略

GC策略適合場景
gencon預設策略,分代收集,效能優秀,適合大部分場合
balanced比gencon更適合處理大物件,更適合對GC暫停時間有較高要求的場合
optavgpause和optthruput適合物件生命週期比較統一的應用,即物件大量一起生一起死的場合
metronome專為需要精確的收集暫停時間上限以及指定應用程式利用率的應用程式而設計

如何使用OpenJ9

如果之前是在使用HotSpot JVM想要嘗試一下OpenJ9,那麼可以參考本章節的建議。

目前OpenJ9支援jdk8,jdk11和jdk17。由於OpenJ9遵循了虛擬機器規範,因此在大部分的場景下不需要過多的變動。

啟動項

要想嘗試OpenJ9,那麼首先需要考慮到的是其啟動項和其他虛擬機器的不同之處。不過OpenJ9在這方面做了相容,絕大部分的HotSpot JVM啟動項都能夠在OpenJ9中直接使用,除了少部分。

堆引數

OpenJ9中所有涉及到堆的設定的引數都是需要注意的,這些引數名稱雖然和HotSpot JVM一樣,但是其包含的意義會有所不同,因為兩者的GC策略會有不同之處。但是可以簡單的將GC策略gencon理解為分代收集,balanced理解為G1,配置就大同小異了。可以參考這些連結:xmn xms

這裡會有一個不同之處,OpenJ9可以透過設定xmo來設定gencon中的tenure的值。

dump

OpenJ9中提供了-Xdump引數,用於進行JVM的診斷,此引數用於替代-XX:HeapDumpPath-XX:+HeapDumpOnOutOfMemory等引數,功能更加強大。當然舊的這些dump引數OpenJ9也做了支援,完全可以不做變動。

等價引數

以下是在HotSpotOpenJ9中等價的引數

HotSpotOpenJ9
-Xcomp-Xjit:count=0
-Xgc-Xgcpolicy
-XX:+UseNUMA-Xnuma:none

GC策略

詳情可以參照上文的GC章節

大致上來說使用預設的GC策略即可,配置也可以使用預設配置。

雲原生支援

OpenJ9提供了-Xtune:virtualized引數來用於雲原生的環境,此設定可以在雲原生環境下以犧牲少量的吞吐量為代價來節省cpu資源。

k8s

在k8s場景下,如果想要使用共享類快取的話需要為pod建立共享儲存卷,來打通不同的pod之間的共享機制。

總結

OpenJ9主打的是節約資源與快速啟動。而在微服務和雲原生廣泛應用的當下,節約資源正是切合了當下很多企業降本增效的想法。如果大家有興趣的話,建議可以嘗試下使用OpenJ9

在新技術與新概念層出不窮的當下,我們面臨的環境與挑戰也與以往有了不同,因此有了一些針對不同場合,為了解決不同問題的JVM應運而生,或許在不久的將來,就不再會是HotSpot獨佔鰲頭,而是各大不同的虛擬機器各領風騷的時代。讓我們不斷關注吧!

參考資料

[1] https://developer.ibm.com/art...

[2] https://developer.ibm.com/tut...

[3] https://www.eclipse.org/openj...

[4] https://www.eclipse.org/openj...

[5] https://www.eclipse.org/openj...

[6] https://blog.openj9.org/2019/...

相關文章