有了HotSpot JVM為什麼還需要OpenJ9?
什麼是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
存在如下特點:
1. 由於region
的特殊性,在一開始就強制限定了對象的最大大小。
2. 對象始終被分配在單個region
內,不會跨region
分配。
3. region
大小始終是2的N次冪,且是在啟動時根據堆的最大值來決定的。
4. 虛擬機總是會生成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/articles/garbage-collection-tradeoffs-and-tuning-with-openj9/
[2] https://developer.ibm.com/tutorials/j-class-sharing-openj9/
[3] https://www.eclipse.org/openj9/docs/aot/
[4] https://www.eclipse.org/openj9/docs/gc/
[5] https://www.eclipse.org/openj9/docs/shrc/
[6] https://blog.openj9.org/2019/05/01/double-map-arraylets/