什麼是OpenJ9
OpenJ9
是一個致力於構建更小記憶體使用,更快啟動速度和更高吞吐量的獨立實現的Java虛擬機器。專案由IBM發起,並在之後開源並捐贈給Eclipse基金會。
為什麼需要OpenJ9
HotSpot JVM
在Java虛擬機器領域獨領風騷多年了,但是近年來有GraalVM
,OpenJ9
等等後起之秀嶄露頭角,開始在各自的領域發力。
正如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載入類時,會分別生成RAMClass
和ROMClass
並儲存在本地的記憶體中。如果開啟了類共享,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
被分為了兩個部分:allocate
與survivor
。GC過程如下圖所示:
- 新物件進入
nursery
的allocate
區域 allocate
漸漸增長直至完全充滿- 本地清掃程式啟動,將所有可達的物件放入到
survivor
,或者如果物件已經到達tenure age
,則直接進入tenure
區域 - 之後
allocate
與survivor
角色互換,先前的allocate
變為survivor
,先前的survivor
則變為allocate
,為下一次GC作準備
allocate
和survivor
的相對大小會根據一種叫做tilting
的動態調整技術來進行變化。剛開始allocate
和survivor
的大小是五五開的,在清理過程中如果發現哪一邊所需的空間較小,會對空間進行動態調整以滿足GC的需求。以此可以儘可能減少GC的週期。
其中tenure age
是指物件在allocate
和survivor
的切換過程中存活下來的次數,JVM會依據此資料來決定物件是否轉移到tenure
。可以透過-Xgc:scvTenureAge=<n>
引數來設定初始的tenure age
,後續的tenure age
可能會隨著GC的程式由JVM進行自適應來最佳化當前的空間使用率。當然如果要關閉tenure age
自適應,可以使用此引數-Xgc:scvNoAdaptiveTenure
。
tenure
預設會被分為兩部分:小物件區域(SOA),大物件區域(LOA),SOA中存放不大於64KB的物件,LOA則相反。如果要禁用LOA,可以使用-Xnoloa
引數。
balanced
balanced
GC策略使用引數-Xgcpolicy:balanced
啟用(需注意此策略僅支援64位平臺)。在此策略下Java堆被分為一個個不同的region
(1024 - 2048),這些region
由增量分代收集器單獨管理,以減少大堆上的最大暫停時間並提高垃圾回收的效率。此策略將堆進行切分以避免全域性的垃圾回收,以此來減少垃圾回收時的長暫停。
balanced
策略類似於HotSpot
中的G1收集器。
在虛擬機器啟動的時候,堆記憶體會被劃分為大小相等的region
,這些region
就是balanced
gc策略的基本單元。
region
存在如下特點:
- 由於
region
的特殊性,在一開始就強制限定了物件的最大大小。 - 物件始終被分配在單個
region
內,不會跨region
分配。 region
大小始終是2的N次冪,且是在啟動時根據堆的最大值來決定的。- 虛擬機器總是會生成1024~2048個
region
基於上述特性我們來看下balanced
gc策略的gc流程。
上圖是堆上的region
的劃分。其中age
為0的是eden
,age
為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-64
和AIX平臺
。此策略是一種具備較短暫停時間的增量的,確定的垃圾回收策略。
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
也做了支援,完全可以不做變動。
等價引數
以下是在HotSpot
與OpenJ9
中等價的引數
HotSpot | OpenJ9 |
---|---|
-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...