溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

java應(yīng)用怎么實現(xiàn)GC優(yōu)化

發(fā)布時間:2021-06-26 09:50:04 來源:億速云 閱讀:127 作者:chen 欄目:大數(shù)據(jù)

這篇文章主要介紹“java應(yīng)用怎么實現(xiàn)GC優(yōu)化”,在日常操作中,相信很多人在java應(yīng)用怎么實現(xiàn)GC優(yōu)化問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”java應(yīng)用怎么實現(xiàn)GC優(yōu)化”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

當(dāng)Java程序性能達(dá)不到既定目標(biāo),且其他優(yōu)化手段都已經(jīng)窮盡時,通常需要調(diào)整垃圾回收器來進(jìn)一步提高性能,稱為GC優(yōu)化。但GC算法復(fù)雜,影響GC性能的參數(shù)眾多,且參數(shù)調(diào)整又依賴于應(yīng)用各自的特點,這些因素很大程度上增加了GC優(yōu)化的難度。即便如此,GC調(diào)優(yōu)也不是無章可循,仍然有一些通用的思考方法。本篇會介紹這些通用的GC優(yōu)化策略和相關(guān)實踐案例,主要包括如下內(nèi)容: > 優(yōu)化前準(zhǔn)備: 簡單回顧JVM相關(guān)知識、介紹GC優(yōu)化的一些通用策略。 > 優(yōu)化方法: 介紹調(diào)優(yōu)的一般流程:明確優(yōu)化目標(biāo)→優(yōu)化→跟蹤優(yōu)化結(jié)果。 > 優(yōu)化案例: 簡述筆者所在團(tuán)隊遇到的GC問題以及優(yōu)化方案。

一、優(yōu)化前的準(zhǔn)備

GC優(yōu)化需知

為了更好地理解本篇所介紹的內(nèi)容,你需要了解如下內(nèi)容。 1. GC相關(guān)基礎(chǔ)知識,包括但不限于: a) GC工作原理。 b) 理解新生代、老年代、晉升等術(shù)語含義。 c) 可以看懂GC日志。

  1. GC優(yōu)化不能解決一切性能問題,它是最后的調(diào)優(yōu)手段。

如果對第一點中提及的知識點不是很熟悉,可以先閱讀小結(jié)-JVM基礎(chǔ)回顧;如果已經(jīng)很熟悉,可以跳過該節(jié)直接往下閱讀。

JVM基礎(chǔ)回顧

JVM內(nèi)存結(jié)構(gòu)

簡單介紹一下JVM內(nèi)存結(jié)構(gòu)和常見的垃圾回收器。

當(dāng)代主流虛擬機(jī)(Hotspot VM)的垃圾回收都采用“分代回收”的算法?!胺执厥铡笔腔谶@樣一個事實:對象的生命周期不同,所以針對不同生命周期的對象可以采取不同的回收方式,以便提高回收效率。

Hotspot VM將內(nèi)存劃分為不同的物理區(qū),就是“分代”思想的體現(xiàn)。如圖所示,JVM內(nèi)存主要由新生代、老年代、永久代構(gòu)成。

java應(yīng)用怎么實現(xiàn)GC優(yōu)化

① 新生代(Young Generation):大多數(shù)對象在新生代中被創(chuàng)建,其中很多對象的生命周期很短。每次新生代的垃圾回收(又稱Minor GC)后只有少量對象存活,所以選用復(fù)制算法,只需要少量的復(fù)制成本就可以完成回收。

新生代內(nèi)又分三個區(qū):一個Eden區(qū),兩個Survivor區(qū)(一般而言),大部分對象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時,還存活的對象將被復(fù)制到兩個Survivor區(qū)(中的一個)。當(dāng)這個Survivor區(qū)滿時,此區(qū)的存活且不滿足“晉升”條件的對象將被復(fù)制到另外一個Survivor區(qū)。對象每經(jīng)歷一次Minor GC,年齡加1,達(dá)到“晉升年齡閾值”后,被放到老年代,這個過程也稱為“晉升”。顯然,“晉升年齡閾值”的大小直接影響著對象在新生代中的停留時間,在Serial和ParNew GC兩種回收器中,“晉升年齡閾值”通過參數(shù)MaxTenuringThreshold設(shè)定,默認(rèn)值為15。

② 老年代(Old Generation):在新生代中經(jīng)歷了N次垃圾回收后仍然存活的對象,就會被放到年老代,該區(qū)域中對象存活率高。老年代的垃圾回收(又稱Major GC)通常使用“標(biāo)記-清理”或“標(biāo)記-整理”算法。整堆包括新生代和老年代的垃圾回收稱為Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都會同時收集整個GC堆,包括新生代)。

③ 永久代(Perm Generation):主要存放元數(shù)據(jù),例如Class、Method的元信息,與垃圾回收要回收的Java對象關(guān)系不大。相對于新生代和年老代來說,該區(qū)域的劃分對垃圾回收影響比較小。

常見垃圾回收器

不同的垃圾回收器,適用于不同的場景。常用的垃圾回收器:

  • 串行(Serial)回收器是單線程的一個回收器,簡單、易實現(xiàn)、效率高。

  • 并行(ParNew)回收器是Serial的多線程版,可以充分的利用CPU資源,減少回收的時間。

  • 吞吐量優(yōu)先(Parallel Scavenge)回收器,側(cè)重于吞吐量的控制。

  • 并發(fā)標(biāo)記清除(CMS,Concurrent Mark Sweep)回收器是一種以獲取最短回收停頓時間為目標(biāo)的回收器,該回收器是基于“標(biāo)記-清除”算法實現(xiàn)的。

GC日志

每一種回收器的日志格式都是由其自身的實現(xiàn)決定的,換而言之,每種回收器的日志格式都可以不一樣。但虛擬機(jī)設(shè)計者為了方便用戶閱讀,將各個回收器的日志都維持一定的共性。JavaGC日志 中簡單介紹了這些共性。

參數(shù)基本策略

各分區(qū)的大小對GC的性能影響很大。如何將各分區(qū)調(diào)整到合適的大小,分析活躍數(shù)據(jù)的大小是很好的切入點。

活躍數(shù)據(jù)的大小是指,應(yīng)用程序穩(wěn)定運(yùn)行時長期存活對象在堆中占用的空間大小,也就是Full GC后堆中老年代占用空間的大小??梢酝ㄟ^GC日志中Full GC之后老年代數(shù)據(jù)大小得出,比較準(zhǔn)確的方法是在程序穩(wěn)定后,多次獲取GC數(shù)據(jù),通過取平均值的方式計算活躍數(shù)據(jù)的大小。活躍數(shù)據(jù)和各分區(qū)之間的比例關(guān)系如下(見參考文獻(xiàn)1):

空間倍數(shù)
總大小3-4 倍活躍數(shù)據(jù)的大小
新生代1-1.5 活躍數(shù)據(jù)的大小
老年代2-3 倍活躍數(shù)據(jù)的大小
永久代1.2-1.5 倍Full GC后的永久代空間占用

例如,根據(jù)GC日志獲得老年代的活躍數(shù)據(jù)大小為300M,那么各分區(qū)大小可以設(shè)為:

總堆:1200MB = 300MB × 4* 新生代:450MB = 300MB × 1.5* 老年代: 750MB = 1200MB - 450MB*

這部分設(shè)置僅僅是堆大小的初始值,后面的優(yōu)化中,可能會調(diào)整這些值,具體情況取決于應(yīng)用程序的特性和需求。

二、優(yōu)化步驟

GC優(yōu)化一般步驟可以概括為:確定目標(biāo)、優(yōu)化參數(shù)、驗收結(jié)果。

確定目標(biāo)

明確應(yīng)用程序的系統(tǒng)需求是性能優(yōu)化的基礎(chǔ),系統(tǒng)的需求是指應(yīng)用程序運(yùn)行時某方面的要求,譬如: - 高可用,可用性達(dá)到幾個9。 - 低延遲,請求必須多少毫秒內(nèi)完成響應(yīng)。 - 高吞吐,每秒完成多少次事務(wù)。

明確系統(tǒng)需求之所以重要,是因為上述性能指標(biāo)間可能沖突。比如通常情況下,縮小延遲的代價是降低吞吐量或者消耗更多的內(nèi)存或者兩者同時發(fā)生。

由于筆者所在團(tuán)隊主要關(guān)注高可用和低延遲兩項指標(biāo),所以接下來分析,如何量化GC時間和頻率對于響應(yīng)時間和可用性的影響。通過這個量化指標(biāo),可以計算出當(dāng)前GC情況對服務(wù)的影響,也能評估出GC優(yōu)化后對響應(yīng)時間的收益,這兩點對于低延遲服務(wù)很重要。

舉例:假設(shè)單位時間T內(nèi)發(fā)生一次持續(xù)25ms的GC,接口平均響應(yīng)時間為50ms,且請求均勻到達(dá),根據(jù)下圖所示:

java應(yīng)用怎么實現(xiàn)GC優(yōu)化

  • 擴(kuò)容前:新生代容量為R ,假設(shè)對象A的存活時間為750ms,Minor GC間隔500ms,那么本次Minor GC時間= T1(掃描新生代R)+T2(復(fù)制對象A到S)。

  • 擴(kuò)容后:新生代容量為2R ,對象A的生命周期為750ms,那么Minor GC間隔增加為1000ms,此時Minor GC對象A已不再存活,不需要把它復(fù)制到Survivor區(qū),那么本次GC時間 = 2 × T1(掃描新生代R),沒有T2復(fù)制時間。

可見,擴(kuò)容后,Minor GC時增加了T1(掃描時間),但省去T2(復(fù)制對象)的時間,更重要的是對于虛擬機(jī)來說,復(fù)制對象的成本要遠(yuǎn)高于掃描成本,所以,單次Minor GC時間更多取決于GC后存活對象的數(shù)量,而非Eden區(qū)的大小。因此如果堆中短期對象很多,那么擴(kuò)容新生代,單次Minor GC時間不會顯著增加。下面需要確認(rèn)下服務(wù)中對象的生命周期分布情況:

java應(yīng)用怎么實現(xiàn)GC優(yōu)化

調(diào)整后:java應(yīng)用怎么實現(xiàn)GC優(yōu)化

優(yōu)化

解決問題前,先回顧一下CMS的四個主要階段,以及各個階段的工作內(nèi)容。下圖展示了CMS各個階段可以標(biāo)記的對象,用不同顏色區(qū)分。 1. Init-mark初始標(biāo)記(STW) ,該階段進(jìn)行可達(dá)性分析,標(biāo)記GC ROOT能直接關(guān)聯(lián)到的對象,所以很快。 2. Concurrent-mark并發(fā)標(biāo)記,由前階段標(biāo)記過的綠色對象出發(fā),所有可到達(dá)的對象都在本階段中標(biāo)記。 3. Remark重標(biāo)記(STW) ,暫停所有用戶線程,重新掃描堆中的對象,進(jìn)行可達(dá)性分析,標(biāo)記活著的對象。因為并發(fā)標(biāo)記階段是和用戶線程并發(fā)執(zhí)行的過程,所以該過程中可能有用戶線程修改某些活躍對象的字段,指向了一個未標(biāo)記過的對象,如下圖中紅色對象在并發(fā)標(biāo)記開始時不可達(dá),但是并行期間引用發(fā)生變化,變?yōu)閷ο罂蛇_(dá),這個階段需要重新標(biāo)記出此類對象,防止在下一階段被清理掉,這個過程也是需要STW的。特別需要注意一點,這個階段是以新生代中對象為根來判斷對象是否存活的。 4. 并發(fā)清理,進(jìn)行并發(fā)的垃圾清理。

java應(yīng)用怎么實現(xiàn)GC優(yōu)化

如果僅掃描老年代中對象,即以老年代中對象為根,判斷對象是否存在引用,上圖中,對象A因為引用存在新生代中,它在Remark階段就不會被修正標(biāo)記為可達(dá),GC時會被錯誤回收。 新生代對象持有老年代中對象的引用,這種情況稱為“跨代引用”。因它的存在,Remark階段必須掃描整個堆來判斷對象是否存活,包括圖中灰色的不可達(dá)對象。

灰色對象已經(jīng)不可達(dá),但仍然需要掃描的原因:新生代GC和老年代的GC是各自分開獨立進(jìn)行的,只有Minor GC時才會使用根搜索算法,標(biāo)記新生代對象是否可達(dá),也就是說雖然一些對象已經(jīng)不可達(dá),但在Minor GC發(fā)生前不會被標(biāo)記為不可達(dá),CMS也無法辨認(rèn)哪些對象存活,只能全堆掃描(新生代+老年代)。由此可見堆中對象的數(shù)目影響了Remark階段耗時。 分析GC日志可以得出同樣的規(guī)律,Remark耗時>500ms時,新生代使用率都在75%以上。這樣降低Remark階段耗時問題轉(zhuǎn)換成如何減少新生代對象數(shù)量。

新生代中對象的特點是“朝生夕滅”,這樣如果Remark前執(zhí)行一次Minor GC,大部分對象就會被回收。CMS就采用了這樣的方式,在Remark前增加了一個可中斷的并發(fā)預(yù)清理(CMS-concurrent-abortable-preclean),該階段主要工作仍然是并發(fā)標(biāo)記對象是否存活,只是這個過程可被中斷。此階段在Eden區(qū)使用超過2M時啟動,當(dāng)然2M是默認(rèn)的閾值,可以通過參數(shù)修改。如果此階段執(zhí)行時等到了Minor GC,那么上述灰色對象將被回收,Reamark階段需要掃描的對象就少了。

除此之外CMS為了避免這個階段沒有等到Minor GC而陷入無限等待,提供了參數(shù)CMSMaxAbortablePrecleanTime ,默認(rèn)為5s,含義是如果可中斷的預(yù)清理執(zhí)行超過5s,不管發(fā)沒發(fā)生Minor GC,都會中止此階段,進(jìn)入Remark。 根據(jù)GC日志紅色標(biāo)記2處顯示,可中斷的并發(fā)預(yù)清理執(zhí)行了5.35s,超過了設(shè)置的5s被中斷,期間沒有等到Minor GC ,所以Remark時新生代中仍然有很多對象。

對于這種情況,CMS提供CMSScavengeBeforeRemark參數(shù),用來保證Remark前強(qiáng)制進(jìn)行一次Minor GC。

優(yōu)化結(jié)果

經(jīng)過增加CMSScavengeBeforeRemark參數(shù),單次執(zhí)行時間>200ms的GC停頓消失,從監(jiān)控上觀察,GCtime和業(yè)務(wù)波動保持一致,不再有明顯的毛刺。java應(yīng)用怎么實現(xiàn)GC優(yōu)化

卡表的具體策略是將老年代的空間分成大小為512B的若干張卡(card)。卡表本身是單字節(jié)數(shù)組,數(shù)組中的每個元素對應(yīng)著一張卡,當(dāng)發(fā)生老年代引用新生代時,虛擬機(jī)將該卡對應(yīng)的卡表元素設(shè)置為適當(dāng)?shù)闹?。如上圖所示,卡表3被標(biāo)記為臟(卡表還有另外的作用,標(biāo)識并發(fā)標(biāo)記階段哪些塊被修改過),之后Minor GC時通過掃描卡表就可以很快的識別哪些卡中存在老年代指向新生代的引用。這樣虛擬機(jī)通過空間換時間的方式,避免了全堆掃描。

總結(jié)來說,CMS的設(shè)計聚焦在獲取最短的時延,為此它“不遺余力”地做了很多工作,包括盡量讓應(yīng)用程序和GC線程并發(fā)、增加可中斷的并發(fā)預(yù)清理階段、引入卡表等,雖然這些操作犧牲了一定吞吐量但獲得了更短的回收停頓時間。

案例三 發(fā)生Stop-The-World的GC

確定目標(biāo)

GC日志如下圖(在GC日志中,F(xiàn)ull GC是用來說明這次垃圾回收的停頓類型,代表STW類型的GC,并不特指老年代GC),根據(jù)GC日志可知本次Full GC耗時1.23s。這個在線服務(wù)同樣要求低時延高可用。本次優(yōu)化目標(biāo)是降低單次STW回收停頓時間,提高可用性。

優(yōu)化

首先,什么時候可能會觸發(fā)STW的Full GC呢? 1. Perm空間不足; 2. CMS GC時出現(xiàn)promotion failed和concurrent mode failure(concurrent mode failure發(fā)生的原因一般是CMS正在進(jìn)行,但是由于老年代空間不足,需要盡快回收老年代里面的不再被使用的對象,這時停止所有的線程,同時終止CMS,直接進(jìn)行Serial Old GC); 3. 統(tǒng)計得到的Young GC晉升到老年代的平均大小大于老年代的剩余空間; 4. 主動觸發(fā)Full GC(執(zhí)行jmap -histo:live [pid])來避免碎片問題。

然后,我們來逐一分析一下: - 排除原因2:如果是原因2中兩種情況,日志中會有特殊標(biāo)識,目前沒有。 - 排除原因3:根據(jù)GC日志,當(dāng)時老年代使用量僅為20%,也不存在大于2G的大對象產(chǎn)生。 - 排除原因4:因為當(dāng)時沒有相關(guān)命令執(zhí)行。 - 鎖定原因1:根據(jù)日志發(fā)現(xiàn)Full GC后,Perm區(qū)變大了,推斷是由于永久代空間不足容量擴(kuò)展導(dǎo)致的。

找到原因后解決方法有兩種: 1. 通過把-XX:PermSize參數(shù)和-XX:MaxPermSize設(shè)置成一樣,強(qiáng)制虛擬機(jī)在啟動的時候就把永久代的容量固定下來,避免運(yùn)行時自動擴(kuò)容。 2. CMS默認(rèn)情況下不會回收Perm區(qū),通過參數(shù)CMSPermGenSweepingEnabled、CMSClassUnloadingEnabled ,可以讓CMS在Perm區(qū)容量不足時對其回收。

由于該服務(wù)沒有生成大量動態(tài)類,回收Perm區(qū)收益不大,所以我們采用方案1,啟動時將Perm區(qū)大小固定,避免進(jìn)行動態(tài)擴(kuò)容。

優(yōu)化結(jié)果

調(diào)整參數(shù)后,服務(wù)不再有Perm區(qū)擴(kuò)容導(dǎo)致的STW GC發(fā)生。

小結(jié)

對于性能要求很高的服務(wù),建議將MaxPermSize和MinPermSize設(shè)置成一致(JDK8開始,Perm區(qū)完全消失,轉(zhuǎn)而使用元空間。而元空間是直接存在內(nèi)存中,不在JVM中),Xms和Xmx也設(shè)置為相同,這樣可以減少內(nèi)存自動擴(kuò)容和收縮帶來的性能損失。虛擬機(jī)啟動的時候就會把參數(shù)中所設(shè)定的內(nèi)存全部化為私有,即使擴(kuò)容前有一部分內(nèi)存不會被用戶代碼用到,這部分內(nèi)存在虛擬機(jī)中被標(biāo)識為虛擬內(nèi)存,也不會交給其他進(jìn)程使用。

四、總結(jié)

結(jié)合上述GC優(yōu)化案例做個總結(jié): 1. 首先再次聲明,在進(jìn)行GC優(yōu)化之前,需要確認(rèn)項目的架構(gòu)和代碼等已經(jīng)沒有優(yōu)化空間。我們不能指望一個系統(tǒng)架構(gòu)有缺陷或者代碼層次優(yōu)化沒有窮盡的應(yīng)用,通過GC優(yōu)化令其性能達(dá)到一個質(zhì)的飛躍。 2. 其次,通過上述分析,可以看出虛擬機(jī)內(nèi)部已有很多優(yōu)化來保證應(yīng)用的穩(wěn)定運(yùn)行,所以不要為了調(diào)優(yōu)而調(diào)優(yōu),不當(dāng)?shù)恼{(diào)優(yōu)可能適得其反。 3. 最后,GC優(yōu)化是一個系統(tǒng)而復(fù)雜的工作,沒有萬能的調(diào)優(yōu)策略可以滿足所有的性能指標(biāo)。GC優(yōu)化必須建立在我們深入理解各種垃圾回收器的基礎(chǔ)上,才能有事半功倍的效果。

本文中案例均來北京業(yè)務(wù)安全中心(也稱風(fēng)控)對接服務(wù)的實踐經(jīng)驗。同時感謝風(fēng)控的小伙伴們,是他們專業(yè)負(fù)責(zé)的審閱,才讓這篇文章更加完善。對于本文中涉及到的內(nèi)容,歡迎大家指正和補(bǔ)充。

到此,關(guān)于“java應(yīng)用怎么實現(xiàn)GC優(yōu)化”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI