溫馨提示×

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

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

如何進(jìn)行一次年輕代GC長暫停問題的解決與思考

發(fā)布時(shí)間:2021-12-06 11:52:21 來源:億速云 閱讀:145 作者:柒染 欄目:大數(shù)據(jù)

如何進(jìn)行一次年輕代GC長暫停問題的解決與思考,相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。

問題描述

公司某規(guī)則引擎系統(tǒng),在每次發(fā)版啟動(dòng)會(huì)手動(dòng)預(yù)熱,預(yù)熱完成當(dāng)流量切進(jìn)來之后會(huì)偶發(fā)的出現(xiàn)一次長達(dá)1-2秒的年輕代GC(流量并不大,并且LB下的每一臺(tái)服務(wù)都會(huì)出現(xiàn)該情況)

在這次長暫停之后,每一次的年輕代GC暫停時(shí)間又都恢復(fù)在20-100ms以內(nèi)

2s雖然看起來不長,但是對(duì)比規(guī)則引擎每次10ms左右的響應(yīng)時(shí)間來說,還是不可以接受的;并且由于該規(guī)則引擎響應(yīng)超時(shí),還會(huì)導(dǎo)致出單超時(shí)失敗

問題分析

在分析該系統(tǒng)GC日志后發(fā)現(xiàn),2s暫停發(fā)生在Young GC階段,而且每次發(fā)生長暫停的Young GC都會(huì)伴隨著新生代對(duì)象的晉升(Promotion)

核心JVM參數(shù)(Oracle JDK7)

-Xms10G
-Xmx10G
-XX:NewSize=4G
-XX:PermSize=1g
-XX:MaxPermSize=4g
-XX:+UseConcMarkSweepGC
復(fù)制代碼

啟動(dòng)后第一次年輕代GC日志

2020-04-23T16:28:31.108+0800: [GC2020-04-23T16:28:31.108+0800: [ParNew2020-04-23T16:28:31.229+0800: [SoftReference, 0 refs, 0.0000950 secs]2020-04-23T16:28:31.229+0800: [WeakReference, 1156 refs, 0.0001040 secs]2020-04-23T16:28:31.229+0800: [FinalReference, 10410 refs, 0.0103720 secs]2020-04-23T16:28:31.240+0800: [PhantomReference, 286 refs, 2 refs, 0.0129420 secs]2020-04-23T16:28:31.253+0800: [JNI Weak Reference, 0.0000000 secs]
Desired survivor size 214728704 bytes, new threshold 1 (max 15)
- age   1:  315529928 bytes,  315529928 total
- age   2:   40956656 bytes,  356486584 total
- age   3:    8408040 bytes,  364894624 total
: 3544342K->374555K(3774912K), 0.1444710 secs] 3544342K->374555K(10066368K), 0.1446290 secs] [Times: user=1.46 sys=0.09, real=0.15 secs]
復(fù)制代碼

長暫停年輕代GC日志

2020-04-23T17:18:28.514+0800: [GC2020-04-23T17:18:28.514+0800: [ParNew2020-04-23T17:18:29.975+0800: [SoftReference, 0 refs, 0.0000660 secs]2020-04-23T17:18:29.975+0800: [WeakReference, 1224 refs, 0.0001400 secs]2020-04-23T17:18:29.975+0800: [FinalReference, 8898 refs, 0.0149670 secs]2020-04-23T17:18:29.990+0800: [PhantomReference, 600 refs, 1 refs, 0.0344300 secs]2020-04-23T17:18:30.025+0800: [JNI Weak Reference, 0.0000210 secs]
Desired survivor size 214728704 bytes, new threshold 15 (max 15)
- age   1:   79203576 bytes,   79203576 total
: 3730075K->304371K(3774912K), 1.5114000 secs] 3730075K->676858K(10066368K), 1.5114870 secs] [Times: user=6.32 sys=0.58, real=1.51 secs]
復(fù)制代碼

從這個(gè)長暫停的GC日志來看,是發(fā)生了晉升的,在Young GC后,有363M+的對(duì)象晉升到了老年代,這個(gè)晉升操作因該就是耗時(shí)原因(ps: 檢查過safepoint原因,不存在異常)

由于日志參數(shù)中沒有配置-XX:+PrintHeapAtGC參數(shù),這里是手動(dòng)計(jì)算的晉升大?。?/p>

年輕代年輕變化 - 全堆容量變化 = 晉升大小
(304371K - 3730075K) - (676858K - 3730075K) = 372487K(363M)

下一次年輕代GC日志

2020-04-23T17:23:39.749+0800: [GC2020-04-23T17:23:39.749+0800: [ParNew2020-04-23T17:23:39.774+0800: [SoftReference, 0 refs, 0.0000500 secs]2020-04-23T17:23:39.774+0800: [WeakReference, 3165 refs, 0.0002720 secs]2020-04-23T17:23:39.774+0800: [FinalReference, 3520 refs, 0.0021520 secs]2020-04-23T17:23:39.776+0800: [PhantomReference, 150 refs, 1 refs, 0.0051910 secs]2020-04-23T17:23:39.782+0800: [JNI Weak Reference, 0.0000100 secs]
Desired survivor size 214728704 bytes, new threshold 15 (max 15)
- age   1:   17076040 bytes,   17076040 total
- age   2:   40832336 bytes,   57908376 total
: 3659891K->90428K(3774912K), 0.0321300 secs] 4032378K->462914K(10066368K), 0.0322210 secs] [Times: user=0.30 sys=0.00, real=0.03 secs]
復(fù)制代碼

乍一看其實(shí)沒什么問題,仔細(xì)想想發(fā)現(xiàn)了一些不正常,為什么程序剛啟動(dòng)第二次gc就發(fā)生了晉升呢

這里應(yīng)該是動(dòng)態(tài)年齡判定導(dǎo)致的,GC中晉升年齡閾值并不是固定的15,而是jvm每次gc后動(dòng)態(tài)計(jì)算的

年輕代晉升機(jī)制

為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無須等到MaxTenuringThreshold中要求的年齡
《深入理解Java虛擬機(jī)》一書中提到,對(duì)象晉升年齡的閾值是動(dòng)態(tài)判定的。

不過經(jīng)查閱其他資料和驗(yàn)證后,發(fā)現(xiàn)此處和《深入理解Java虛擬機(jī)》解釋的有些出入(或者是書上解釋的不夠清楚)

其實(shí)就是按年齡給對(duì)象分組,取total(累加值,小于等與當(dāng)前年齡的對(duì)象總大?。┳畲蟮哪挲g分組,如果該分組的total大于survivor的一半,就將晉升年齡閾值更新為該分組的年齡

注意:不是是超過survivor一半就晉升,超過survivor一半只會(huì)重新設(shè)置晉升閾值(threshold),在下一次GC才會(huì)使用該新閾值

3544342K->374555K(3774912K), 0.1444710 secs] 年輕代

3544342K->374555K(10066368K), 0.1446290 secs] 全堆
復(fù)制代碼

從上面第一次的GC日志也可以證明這個(gè)結(jié)論,在這次GC中全堆的內(nèi)存變化和年輕代內(nèi)存變化是相等的,所以并沒有發(fā)生對(duì)象的晉升

就像上面的日志中,第一次GC只是將threshold設(shè)置為1,因?yàn)榇藭r(shí)survivor一半為214728704 bytes,而年齡為1的對(duì)象總和有315529928 bytes,超過了Desired survivor size,所以在本次GC后將threshold設(shè)置為年齡為1的對(duì)象年齡1

這里更新了對(duì)象晉升年齡閾值為1

Desired survivor size 214728704 bytes, new threshold 1 (max 15)
- age   1:  315529928 bytes,  315529928 total
- age   2:   40956656 bytes,  356486584 total
- age   3:    8408040 bytes,  364894624 total
復(fù)制代碼

這里順便解釋下這個(gè)年齡分布的輸出內(nèi)容:

- age 1: 315529928 bytes, 315529928 total
復(fù)制代碼
  • age 1表示年齡為1的對(duì)象分組,315529928 bytes表示年齡為1的對(duì)象占用內(nèi)存大小

315529928 total這個(gè)是一個(gè)累加值,表示小于等于當(dāng)前分組年齡的對(duì)象總大小。先把對(duì)象按年齡分組,age 1的分組total為age 1總大?。ㄇ懊娴膞xx bytes),age 2的分組total為age 1 + age 2總大小,age n的分組total為age 1 + age 2 + ... +age n的總大小,累加規(guī)則如下圖所示

如何進(jìn)行一次年輕代GC長暫停問題的解決與思考

當(dāng)total最大的分組的total值超過了survivor/2時(shí),就會(huì)更新晉升閾值

在第二次年輕代GC“長暫停年輕代GC日志”中,由于新的晉升年齡閾值為1,所以那些經(jīng)歷了一次GC并存活并且現(xiàn)在仍然可達(dá)(reachable)的對(duì)象們就會(huì)發(fā)生晉升了

由于此次GC發(fā)生了363M的對(duì)象晉升,所以導(dǎo)致了長暫停

思考

JVM中這個(gè)“動(dòng)態(tài)對(duì)象年齡判定”真的是合理的嗎?個(gè)人認(rèn)為機(jī)制是好的,可以更好的適應(yīng)不同程序的內(nèi)存狀況,但不是任何場(chǎng)景都適合,比如在本文中這個(gè)剛啟動(dòng)不就GC的場(chǎng)景下就會(huì)有問題

因?yàn)樵诔绦騽倖?dòng)時(shí),大多數(shù)對(duì)象年齡都是0或者1,很容易出現(xiàn)年齡為1的大量存活對(duì)象;在這個(gè)“動(dòng)態(tài)對(duì)象年齡判定”機(jī)制下,就會(huì)導(dǎo)致新的晉升閾值被設(shè)置為1,導(dǎo)致這些不該晉升的對(duì)象發(fā)生了晉升

比如程序在初始化,正在加載各種資源時(shí)發(fā)生了Young GC,加載邏輯還在執(zhí)行中,很多新建的對(duì)象年齡在這次GC時(shí)還是可達(dá)的(reachable)

經(jīng)歷了這次GC后,這些對(duì)象年齡更新為1,但是由于“動(dòng)態(tài)對(duì)象年齡判定”機(jī)制的影響,晉升年齡閾值更新為了“最大的對(duì)象年齡分組”的年齡,也就是這批剛經(jīng)歷了一次GC的對(duì)象們

在這次GC之后不久,資源初始化完成了,涉及的相關(guān)對(duì)象有很可能不可達(dá)了,但是由于剛才晉升年齡閾值被更新為了1,在下一次正常的Young GC這批年齡為1的對(duì)象會(huì)直接發(fā)生晉升,提前或者說錯(cuò)誤的發(fā)生了晉升

解決方案

經(jīng)查閱文檔、資料,發(fā)現(xiàn)“動(dòng)態(tài)年齡判定”這個(gè)機(jī)制并不能禁用,所以如果想解決這個(gè)問題,只有靠“繞過”這個(gè)計(jì)算規(guī)則了

動(dòng)態(tài)年齡的判定,是根據(jù)Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半來判定的,那么根據(jù)這個(gè)機(jī)制解決也很簡單

由于我們足夠了解自己的系統(tǒng),清楚的知道加載資源所需的大概內(nèi)存,完全可以設(shè)定一個(gè)大于這些暫時(shí)可達(dá)的對(duì)象總和的數(shù)值來作為survivor的容量

比如上面的日志中,第一次GC后年齡為1的對(duì)象有315529928 Bytes(300M),Desired survivor size為(survivor size /2)214728704 bytes(204M),那么survivor就可以設(shè)置為600M以上。

不過為了穩(wěn)妥,還是將survivor調(diào)到800M,這樣desired survivor size就是400M左右,在第一次Young GC后,就不會(huì)因年齡為1的對(duì)象總和超過了desired survivor size而導(dǎo)致晉升年齡閾值的更新了,從而也就不會(huì)有提前/錯(cuò)誤晉升而導(dǎo)致的GC長暫停問題

survivor不可以直接指定大小,不過可以通過-XX:SurvivorRatio這種調(diào)節(jié)比例的方式來調(diào)節(jié)survivor大小

-XX:SurvivorRatio=8

表示兩個(gè)Survivor和Edgen區(qū)的比,8表示兩個(gè)Survivor:Eden=2:8,即一個(gè)Survivor占新生代的1/10。

計(jì)算方式為:

Survivor Size(1) = Young Generation Size / (2+SurvivorRatio)
Eden Size = Young Generation Size / (2+SurvivorRatio) * SurvivorRatio
復(fù)制代碼

擴(kuò)展閱讀

為什么晉升300M比年輕代回收3G還要慢這么多倍
根據(jù)復(fù)制算法的特性,復(fù)制算法的時(shí)間消耗主要取決于存活對(duì)象的大小,而不是總空間的大小

比如上面4G的年輕代(實(shí)際只有Eden+S0可用),GC時(shí)只需要從GC ROOTS開始遍歷對(duì)象圖,將可達(dá)的對(duì)象復(fù)制至S1即可,并不需要遍歷整個(gè)年輕代

在上面那次長暫停GC日志中,發(fā)生了363M的晉升,300M左右的回收,對(duì)比第一次GC基本可以得出,花費(fèi)的1.5S基本上都是在晉升操作

那么為什么晉升操作這么耗時(shí)呢?

這里沒有深入研究Oracle JVM實(shí)現(xiàn)的年輕代晉升細(xì)節(jié),不過晉升涉及跨代復(fù)制(其實(shí)都年輕代和老年代都是heap,在復(fù)制這件事上本質(zhì)上沒什么區(qū)別,都是memcpy而已,只是需要額外處理的邏輯更多了)
,所需處理的邏輯會(huì)更復(fù)雜一些,比如指針的更新等操作,更耗時(shí)也是可以理解的,

本地代碼模擬

這里也附上一段可以在本地模擬問題的代碼,Oracle JDK7下可直接運(yùn)行測(cè)試

//jdk7.。

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class PromotionTest {
   public static void main(String[] args) throws IOException {
       //模擬初始化資源場(chǎng)景
       List<Object> dataList = new ArrayList<>();
       for (int i = 0; i < 5; i++) {
           dataList.add(new InnerObject());
       }
       //模擬流量進(jìn)入場(chǎng)景
       for (int i = 0; i < 73; i++) {
           if(i == 72){
               System.out.println("Execute young gc...Adjust promotion threshold to 1");
           }
           new InnerObject();
       }
       System.out.println("Execute full gc...dataList has been promoted to cms old space");
       //這里注意dataList中的對(duì)象在這次Full GC后會(huì)進(jìn)入老年代
       System.gc();
   }
   public static byte[] createData(){
       int dataSize = 1024*1024*4;//4m
       byte[] data = new byte[dataSize];
       for (int j = 0; j < dataSize; j++) {
           data[j] = 1;
       }
       return data;
   }
   static class InnerObject{
       private Object data;

       public InnerObject() {
           this.data = createData();
       }
   }
}
復(fù)制代碼

jvm options

-server -Xmn400M -XX:SurvivorRatio=9 -Xms1000M -Xmx1000M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC -XX:+PrintReferenceGC -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC

看完上述內(nèi)容,你們掌握如何進(jìn)行一次年輕代GC長暫停問題的解決與思考的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

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

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

gc
AI