在高併發下,Java程序的GC問題屬於很典型的一類問題,帶來的影響每每會被進一步放大。無論是「GC頻率過快」仍是「GC耗時太長」,因爲GC期間都存在Stop The World問題,所以很容易致使服務超時,引起性能問題。算法
咱們團隊負責的廣告系統承接了比較大的C端流量,平峯期間的請求量基本達到了上千QPS,過去也遇到了不少次GC相關的線上問題。緩存
5月份的這篇文章我介紹了一個Full GC過於頻繁的案例,而且針對JVM的堆內存結構和GC原理進行了系統性的總結。服務器
這篇文章,我再分享一個更棘手的Young GC耗時過長的線上案例,同時會整理下YGC相關的知識點,但願讓你有所收穫。內容分紅如下2個部分:數據結構
今年4月份,咱們的廣告服務在新版本上線後,收到了大量的服務超時告警,經過下面的監控圖能夠看到:超時量忽然大面積增長,1分鐘內甚至達到了上千次接口超時。下面詳細介紹下該問題的排查過程。多線程
收到告警後,咱們第一時間查看了監控系統,立馬發現了YoungGC耗時過長的異常。 咱們的 程序大概在 21 點50 左右上線,經過下圖能夠看出: 在上線以前,YGC基本幾十毫秒內完成,而上線後YGC耗時明顯變長,最長甚至達到了3秒多。架構
因爲 YGC期間程序會 Stop The World ,而咱們上游系統設置的服務超時時間都在幾百毫秒,所以推斷:是由於YGC耗時過長引起了服務大面積超時。併發
按照GC問題的常規排查流程,咱們馬上摘掉了一個節點,而後經過如下命令dump了堆內存文件用來保留現場。app
jmap -dump:format=b,file=heap pidjvm
最後對線上服務作了回滾處理,回滾後服務立馬恢復了正常,接下來就是長達1天的問題排查和修復過程。高併發
用下面的命令,咱們再次檢查了JVM的參數
ps aux | grep "applicationName=adsearch"
-Xms4g -Xmx4g -Xmn2g -Xss1024K
-XX:ParallelGCThreads=5
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80
能夠看到堆內存爲4G,新生代和老年代均爲2G,新生代採用ParNew收集器。
再經過命令 jmap -heap pid 查到:新生代的Eden區爲1.6G,S0和S1區均爲0.2G。
本次上線並未修改JVM相關的任何參數,同時咱們服務的請求量基本和往常持平。所以猜想:此問題大機率和上線的代碼相關。
再回到YGC的原理來思考這個問題,一次YGC的過程主要包括如下兩個步驟:
一、從GC Root掃描對象,對存活對象進行標註
二、將存活對象複製到S1區或者晉升到Old區
根據下面的監控圖能夠看出:正常狀況下,Survivor區的使用率一直維持在很低的水平(大概30M左右),可是上線後,Survivor區的使用率開始波動,最多的時候快佔滿0.2G了。並且,YGC耗時和Survivor區的使用率基本成正相關。所以,咱們推測:應該是長生命週期的對象愈來愈多,致使標註和複製過程的耗時增長。
再回到服務的總體表現:上游流量並無出現明顯變化,正常狀況下,核心接口的響應時間也基本在200ms之內,YGC的頻率大概每8秒進行1次。
很顯然,對於局部變量來講,在每次YGC後就可以立刻被回收了。那爲何還會有如此多的對象在YGC後存活下來呢?
咱們進一步將懷疑對象鎖定在:程序的全局變量或者類靜態變量上。可是diff了本次上線的代碼,咱們並未發現代碼中有引入此類變量。
代碼排查沒有進展後,咱們開始從堆內存文件中尋找線索,使用MAT工具導入了第1步dump出來的堆文件後,而後經過Dominator Tree視圖查看到了當前堆中的全部大對象。
立馬發現NewOldMappingService這個類所佔的空間很大,經過代碼定位到:這個類位於第三方的client包中,由咱們公司的商品團隊提供,用於實現新舊類目轉換(最近商品團隊在對類目體系進行改造,爲了兼容舊業務,須要進行新舊類目映射)。
進一步查看代碼,發現這個類中存在大量的靜態HashMap,用於緩存新舊類目轉換時須要用到的各類數據,以減小RPC調用,提升轉換性能。
本來覺得,很是接近問題的真相了,可是深刻排查發現:這個類的全部靜態變量所有在類加載時就初始化完數據了,雖然會佔到100多M的內存,可是以後基本不會再新增數據。而且,這個類早在3月份就上線使用了,client包的版本也一直沒變過。
通過上面種種分析,這個類的靜態HashMap會一直存活,通過多輪YGC後,最終晉升到老年代中,它不該該是YGC持續耗時過長的緣由。所以,咱們暫時排除了這個可疑點。
團隊對於YGC問題的排查經驗不多,不知道再往下該如何分析了。基本掃光了網上可查到的全部案例,發現緣由集中在這兩類上:
一、對存活對象標註時間過長:好比重載了Object類的Finalize方法,致使標註Final Reference耗時過長;或者String.intern方法使用不當,致使YGC掃描StringTable時間過長。
二、長週期對象積累過多:好比本地緩存使用不當,積累了太多存活對象;或者鎖競爭嚴重致使線程阻塞,局部變量的生命週期變長。
針對第1類問題,能夠經過如下參數顯示GC處理Reference的耗時-XX:+PrintReferenceGC。 添加此參數後,能夠看到不一樣類型的 reference 處理耗時都很短,所以又排除了此項因素。
再日後,咱們添加了各類GC參數試圖尋找線索都沒有結果,彷佛要黔驢技窮,沒有思路了。綜合監控和種種分析來看:應該只有長週期對象纔會引起咱們這個問題。
折騰了好幾個小時,最終峯迴路轉,一個小夥伴從新從MAT堆內存中找到了第二個懷疑點。
從上面的截圖能夠看到:大對象中排在第3位的ConfigService類進入了咱們的視野,該類的一個ArrayList變量中居然包含了270W個對象,並且大部分都是相同的元素。
ConfigService這個類在第三方Apollo的包中,不過源代碼被公司架構部進行了二次改造,經過代碼能夠看出:問題出在了第11行,每次調用getConfig方法時都會往List中添加元素,而且未作去重處理。
咱們的廣告服務在apollo中存儲了大量的廣告策略配置,並且大部分請求都會調用ConfigService的getConfig方法來獲取配置,所以會不斷地往靜態變量namespaces中添加新對象,從而引起此問題。
至此,整個問題終於水落石出了。這個BUG是由於架構部在對apollo client包進行定製化開發時不當心引入的,很顯然沒有通過仔細測試,而且恰好在咱們上線前一天發佈到了中央倉庫中,而公司基礎組件庫的版本是經過super-pom方式統一維護的,業務無感知。
爲了快速驗證YGC耗時過長是由於此問題致使的,咱們在一臺服務器上直接用舊版本的apollo client 包進行了替換,而後重啓了服務,觀察了將近20分鐘,YGC恢復正常。
最後,咱們 通知架構部修復BUG,從新發布了super-pom ,完全解決了這個問題。
經過上面這個案例,能夠看到YGC問題其實比較難排查。相比FGC或者OOM,YGC的日誌很簡單,只知道新生代內存的變化和耗時,同時dump出來的堆內存必需要仔細排查才行。
另外,若是不清楚YGC的流程,排查起來會更加困難。這裏,我對YGC相關的知識點再作下梳理,方便你們更全面的理解YGC。
YGC 在新生代中進行,首先要清楚新生代的堆結構劃分。新生代分爲Eden區和兩個Survivor區,其中Eden:from:to = 8:1:1 (比例能夠經過參數 –XX:SurvivorRatio 來設定 ),這是最基本的認識。
爲何會有新生代?
若是不分代,全部對象所有在一個區域,每次GC都須要對全堆進行掃描,存在效率問題。分代後,可分別控制回收頻率,並採用不一樣的回收算法,確保GC性能全局最優。
爲何新生代會採用複製算法?
新生代的對象朝生夕死,大約90%的新建對象能夠被很快回收,複製算法成本低,同時還能保證空間沒有碎片。雖然標記整理算法也能夠保證沒有碎片,可是因爲新生代要清理的對象數量很大,將存活的對象整理到待清理對象以前,須要大量的移動操做,時間複雜度比複製算法高。
爲何新生代須要兩個Survivor區?
爲了節省空間考慮,若是採用傳統的複製算法,只有一個Survivor區,則Survivor區大小須要等於Eden區大小,此時空間消耗是8 * 2,而兩塊Survivor能夠保持新對象始終在Eden區建立,存活對象在Survivor之間轉移便可,空間消耗是8+1+1,明顯後者的空間利用率更高。
新生代的實際可用空間是多少?
YGC後,總有一塊Survivor區是空閒的,所以新生代的可用內存空間是90%。在YGC的log中或者經過 jmap -heap pid 命令查看新生代的空間時,若是發現capacity只有90%,不要以爲奇怪。
Eden區是如何加速內存分配的?
HotSpot虛擬機使用了兩種技術來加快內存分配。分別是bump-the-pointer和TLAB(Thread Local Allocation Buffers)。
因爲Eden區是連續的,所以bump-the-pointer在對象建立時,只須要檢查最後一個對象後面是否有足夠的內存便可,從而加快內存分配速度。
TLAB技術是對於多線程而言的,在Eden中爲每一個線程分配一塊區域,減小內存分配時的鎖衝突,加快內存分配速度,提高吞吐量。
SerialGC(串行回收器),最古老的一種,單線程執行,適合單CPU場景。
ParNew(並行回收器),將串行回收器多線程化,適合多CPU場景,須要搭配老年代CMS回收器一塊兒使用。
ParallelGC(並行回收器),和ParNew不一樣點在於它關注吞吐量,可設置指望的停頓時間,它在工做時會自動調整堆大小和其餘參數。
G1(Garage-First回收器),JDK 9及之後版本的默認回收器,兼顧新生代和老年代,將堆拆成一系列Region,不要求內存塊連續,新生代仍然是並行收集。
上述回收器均採用複製算法,都是獨佔式的,執行期間都會Stop The World.
當Eden區空間不足時,就會觸發YGC。結合新生代對象的內存分配看下詳細過程:
一、新對象會先嚐試在棧上分配,若是不行則嘗試在TLAB分配,不然再看是否知足大對象條件要在老年代分配,最後才考慮在Eden區申請空間。
二、若是Eden區沒有合適的空間,則觸發YGC。
三、YGC時,對Eden區和From Survivor區的存活對象進行處理,若是知足動態年齡判斷的條件或者To Survivor區空間不夠則直接進入老年代,若是老年代空間也不夠了,則會發生promotion failed,觸發老年代的回收。不然將存活對象複製到To Survivor區。
四、此時Eden區和From Survivor區的剩餘對象均爲垃圾對象,可直接抹掉回收。
此外,老年代若是採用的是CMS回收器,爲了減小CMS Remark階段的耗時,也有可能會觸發一次YGC,這裏不做展開。
YGC採用的複製算法,主要分紅如下兩個步驟:
一、查找GC Roots,將其引用的對象拷貝到S1區
二、遞歸遍歷第1步的對象,拷貝其引用的對象到S1區或者晉升到Old區
上述整個過程都是須要暫停業務線程的(STW),不過ParNew等新生代回收器能夠多線程並行執行,提升處理效率。
YGC經過可達性分析算法,從GC Root(可達對象的起點)開始向下搜索,標記出當前存活的對象,那麼剩下未被標記的對象就是須要回收的對象。
可做爲YGC時GC Root的對象包括如下幾種:
一、虛擬機棧中引用的對象
二、方法區中靜態屬性、常量引用的對象
三、本地方法棧中引用的對象
四、被Synchronized鎖持有的對象
五、記錄當前被加載類的SystemDictionary
六、記錄字符串常量引用的StringTable
七、存在跨代引用的對象
八、和GC Root處於同一CardTable的對象
其中1-3是你們容易想到的,而4-8很容易被忽視,卻極有多是分析YGC問題時的線索入口。
另外須要注意的是,針對下圖中跨代引用的狀況,老年代的對象A也必須做爲GC Root的一部分,可是若是每次YGC時都去掃描老年代,確定存在效率問題。在HotSpot JVM,引入卡表(Card Table)來對跨代引用的標記進行加速。
Card Table,簡單理解是一種空間換時間的思路,由於存在跨代引用的對象大概佔比不到1%,所以可將堆空間劃分紅大小爲512字節的卡頁,若是卡頁中有一個對象存在跨代引用,則能夠用1個字節來標識該卡頁是dirty狀態,卡頁狀態進一步經過寫屏障技術進行維護。
遍歷完GC Roots後,便可以找出第一批存活的對象,而後將其拷貝到S1區。接下來,就是一個遞歸查找和拷貝存活對象的過程。
S1區爲了方便維護內存區域,引入了兩個指針變量: _saved_mark_word和_top,其中_saved_mark_word表示當前遍歷對象的位置,_top表示當前可分配內存的位置,很顯然,_saved_mark_word到_top之間的對象都是已拷貝但未掃描的對象。
如上圖所示,每次掃描完一個對象,_saved_mark_word會往前移動,期間若是有新對象也會拷貝到S1區,_top也會往前移動,直到_saved_mark_word追上_top,說明S1區全部對象都已經遍歷完成。
有一個細節點須要注意的是:拷貝對象的目標空間不必定是S1區,也多是老年代。若是一個對象的年齡(經歷的YGC次數)知足動態年齡斷定條件便直接晉升到老年代中。對象的年齡保存在Java對象頭的mark word數據結構中(若是你們對Java併發鎖熟悉,確定瞭解這個數據結構,不熟悉的建議查閱資料瞭解下,這裏不作展開)。
這篇文章經過線上案例分析並結合原理講解,詳細介紹了YGC的相關知識。從YGC實戰角度出發,再 簡單總結一下:
一、首先要清楚YGC的執行原理,好比年輕代的堆內存結構、Eden區的內存分配機制、GC Roots掃描、對象拷貝過程等。
二、YGC的核心步驟是標註和複製,絕部分YGC問題都集中在這兩步,所以能夠結合YGC日誌和堆內存變化狀況逐一排查,同時d ump的堆內存文件 須要仔細分析 。
若是你們對JVM性能調優和GC案例感興趣,建議關注前阿里大牛「你假笨」建立的PerfMa社區,裏面有不少高質量的JVM文章。
做者簡介:985碩士,前亞馬遜工程師,現58轉轉技術總監
歡迎關注個人我的公衆號:IT人的職場進階