在軟件開發(fā)中,很明顯,與大型應(yīng)用程序相比,小而靈活的微服務(wù)可以提供更多的優(yōu)勢(shì)。而JDK9的Jigsaw更加有助于分解我們的Java應(yīng)用程序,從而構(gòu)建更適合云原生的應(yīng)用程序和微服務(wù)。而隨著服務(wù)的用戶越來(lái)越多,我們的應(yīng)用程序需要水平擴(kuò)容。在這個(gè)擴(kuò)容過(guò)程中,其在單個(gè)容器中的預(yù)配置資源消耗將復(fù)制到每個(gè)實(shí)例。如此一來(lái),正確配置垃圾回收器,這個(gè)Java程序最基礎(chǔ)的組件,將會(huì)很大程度的影響整個(gè)項(xiàng)目對(duì)資源的利用率。
本文對(duì)比測(cè)試?yán)厥掌鞯膬?nèi)存伸縮能力,總計(jì)覆蓋5大GC:
- G1
- Parallel
- ConcMarkSweep (CMS)
- Serial
- Shenandoah
為了測(cè)試需要,準(zhǔn)備了一段示例代碼,github地址為: https://github.com/jelastic/java-vertical-scaling-test。代碼非常簡(jiǎn)單,只有3個(gè)java類:
- MemoryUsage.java:每隔3秒利用MemoryUsage輸出used/committed/max內(nèi)存。
- Load.java:不斷分配內(nèi)存,且每分配100次會(huì)sleep一段時(shí)間。
- Test.java:不斷調(diào)用Load.java分配內(nèi)存。
測(cè)試過(guò)程中配置的JVM參數(shù)如下:
java -XX:+Use[gc_name]GC -Xmx2g -Xms32m -jar app.jar [sleep]
說(shuō)明:
- gc_name:表示指定的具體垃圾回收器;
- Xms:初始化內(nèi)存大小;
- Xmx:內(nèi)存使用上限;
- sleep:即控制Load.java的sleep時(shí)間;
G1GC
我們首先要測(cè)試的就是G1,從JDK9以后,G1已經(jīng)取代ParallelGC成為默認(rèn)的GC。如果想在低版本JDK中使用G1,那么需要通過(guò)參數(shù)-XX:+UseG1GC顯示指定GC。
G1最大的優(yōu)勢(shì)就是能壓縮空閑內(nèi)存,而且不需要很長(zhǎng)的停頓時(shí)間。并且能回收不再使用的內(nèi)存。經(jīng)過(guò)測(cè)試發(fā)現(xiàn),G1也是這方面表現(xiàn)最好的垃圾回收器。
現(xiàn)在,我們對(duì)G1進(jìn)行3次測(cè)試,測(cè)試的sleep分別為0,10,100。即表示內(nèi)存分配速度由快到慢。
- 快速分配內(nèi)存
對(duì)應(yīng)的JVM參數(shù)如下,sleep為0,即持續(xù)不斷的分配內(nèi)存:
java -XX:+UseG1GC -Xmx2g -Xms32m -jar app.jar 0
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

由圖可知,發(fā)生FGC后,Reserved和Used內(nèi)存非常及時(shí)的下降到最低水平。所以:G1的表現(xiàn)非常好,不再使用的內(nèi)存歸還給操作系統(tǒng)的速度非???/strong>。
- 中速分配內(nèi)存
對(duì)應(yīng)的JVM參數(shù)如下,sleep為10:
java -XX:+UseG1GC -Xmx2g -Xms32m -jar app.jar 10
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

- 慢速分配內(nèi)存
對(duì)應(yīng)的JVM參數(shù)如下,sleep為100:
java -XX:+UseG1GC -Xmx2g -Xms32m -jar app.jar 100
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

由圖可知,Reserved內(nèi)存和Used內(nèi)存幾乎完全同步,因?yàn)榭梢缘贸鼋Y(jié)論:G1并不會(huì)過(guò)多的申請(qǐng)內(nèi)存。
- AggressiveHeap
再做一個(gè)測(cè)試,配置JVM參數(shù)-XX:+AggressiveHeap,或者認(rèn)為Xmx和Xms一樣大,對(duì)應(yīng)的JVM參數(shù)如下:
java -XX:+UseG1GC -Xmx2g -Xms2g
或者
java -XX:+UseG1GC -Xmx2g -XX:+AggressiveHeap
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

由圖可知,Reserved內(nèi)存一直和Xmx一樣大,不會(huì)有任何改變,即使你的程序可能只需要幾十M甚至更少的內(nèi)存,JVM也不會(huì)把那些未使用的可用內(nèi)存歸還給操作系統(tǒng)。如此一來(lái),其他程序就無(wú)法使用這些這些內(nèi)存。
所以,如果想要你的應(yīng)用程序能彈性使用內(nèi)存,確保沒(méi)有配置-XX:+AggressiveHeap,或者Xms小于Xmx。
ParallelGC
ParallelGC以高吞吐量為主要目標(biāo),我們配置如下JVM參數(shù):
java -XX:+UseParallelGC -Xmx2g -Xms32m -jar app.jar 10
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

由圖可知,F(xiàn)GC后未使用的內(nèi)存沒(méi)有被歸還給操作系統(tǒng)(Reserved內(nèi)存曲線并沒(méi)有下降)。配置了ParallelGC的JVM會(huì)一直持有這些內(nèi)存,除非發(fā)生了重啟行為。所以,ParallelGC是不具備伸縮能力的垃圾回收器。
Serial&CMS
Serial和CMS都是可伸縮的垃圾回收器。但是效果與G1相比有一定的差距,它們需要4次FGC周期才能釋放所有不再使用的內(nèi)存。我們配置如下JVM參數(shù):
java -XX:+UseSerialGC -Xmx2g -Xms32m -jar app.jar 10
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

配置CMS也是類似的效果,JVM參數(shù)如下:
java -XX:+UseConcMarkSweepGC -Xmx2g -Xms32m -jar app.jar 10
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

說(shuō)明:從JDK9開始,不再使用的內(nèi)存可以通過(guò)JVM參數(shù)-XX:-ShrinkHeapInSteps,在第一次FGC后加速釋放。
ShenandoahGC
ShenandoahGC是JDK12推出的基于Region設(shè)計(jì)的全新垃圾回收器。它非常大的不同就是:ShenandoahGC不需要FGC就能異步回收不再使用的內(nèi)存并歸還給操作系統(tǒng)。Shenandoah在探測(cè)到可用內(nèi)存后,幾乎能夠立即清理垃圾然后把這部分內(nèi)存歸還給操作系統(tǒng)。讓我們配置如下的JVM參數(shù):
java -XX:+UseShenandoahGC -Xmx2g -Xms32m
-XX:+UnlockExperimentalVMOptions
-XX:ShenandoahUncommitDelay=1000
-XX:ShenandoahGuaranteedGCInterval=10000 -jar app.jar 10
對(duì)應(yīng)的內(nèi)存趨勢(shì)圖如下所示:

由圖可知,ShenandoahGC非常彈性(elastic),即使沒(méi)有發(fā)生任何FGC,它也能馬上把不再使用的內(nèi)存歸還給操作系統(tǒng)。
總結(jié)
根據(jù)上面的測(cè)試結(jié)果可知,只有ParallelGC不具備內(nèi)存伸縮能力。而其他的GC,例如:Serial,CMS,G1,ShenandoahGC都具備內(nèi)存伸縮能力。其中Serial和CMS的效果一般,G1需要借助FGC才能將不再使用的內(nèi)存歸還給操作系統(tǒng)。至于JDK12帶來(lái)的ShenandoahGC,效果非常好,而且不需要依賴FGC,異步就能完成完成內(nèi)存伸縮。
Java不斷完善和適應(yīng)不斷變化的需求。 因此,其對(duì)內(nèi)存的貪婪對(duì)微服務(wù)和云托管程序來(lái)說(shuō)不再是問(wèn)題,因?yàn)橐呀?jīng)有正確的工具和方法來(lái)正確地?cái)U(kuò)展它,清理垃圾并釋放進(jìn)程的資源。 通過(guò)合理的配置,Java對(duì)于所有項(xiàng)目都具有成本效益 - 從云原生到傳統(tǒng)企業(yè)應(yīng)用程序。