JVM性能調(diào)優(yōu)--YGC

YGC
YGC頻次暫且忽略,問(wèn)題主要集中在gc耗時(shí)上面。想要解決YGC耗時(shí)問(wèn)題,首先要搞清楚YGC的耗時(shí)節(jié)點(diǎn)。(據(jù)我所知的YGC問(wèn)題,還沒(méi)有逃出過(guò)這些維度)
存活對(duì)象掃描、標(biāo)記時(shí)間 存活對(duì)象copy to S區(qū),晉升到Old區(qū) 等待各線程到達(dá)安全點(diǎn)時(shí)間 GC日志輸出 操作系統(tǒng)活動(dòng)(swap)
1、等待線程到達(dá)安全點(diǎn)
GC發(fā)生時(shí),程序是會(huì)STW的(Serial, ParNew, Parallel Scanvange, ParallelOld, Serial Old全程都會(huì)STW,CMS等在初始標(biāo)記重新標(biāo)記階段也會(huì)STW), JVM這時(shí)候只運(yùn)行GC線程,不運(yùn)行用戶線程。
那JVM具體要在哪里,在什么時(shí)間點(diǎn)STW呢? 答:安全點(diǎn)。
因此,GC時(shí),程序需要運(yùn)行到最近的一個(gè)安全點(diǎn)(方法返回、循環(huán)結(jié)束、異常拋出等位置)停下來(lái),安全點(diǎn)日志前面文章也提到了:
如果發(fā)現(xiàn) spin時(shí)間段表現(xiàn)異常,那么就要去查看下我們的代碼中是否有影響線程快速到達(dá)安全點(diǎn)的邏輯塊,比如大循環(huán)體等。
順便提一下,安全點(diǎn)的作用不只是進(jìn)行GC,下面這些點(diǎn)都是會(huì)導(dǎo)致程序進(jìn)入安全點(diǎn)的,其相關(guān)問(wèn)題我們可以單開(kāi)一篇來(lái)敘述
2、存活對(duì)象掃描和標(biāo)記
在程序進(jìn)入安全點(diǎn)之后,下一步操作就是root對(duì)象的標(biāo)記和引用對(duì)象的可達(dá)性分析。
2.1 線程棧中的對(duì)象引用
這里需要引出兩個(gè)概念,OopMap和RememberedSet
為了獲取root對(duì)象,jvm需要從線程棧中找到堆內(nèi)存對(duì)象的引用,但是不一定都是引用,如果全棧掃描那就太浪費(fèi)時(shí)間了,怎么辦,空間換時(shí)間(這個(gè)思路簡(jiǎn)直是到處在用,在之前一篇面試相關(guān)的文章中我也有提到),
用OopMap維護(hù)對(duì)象引用,在到達(dá)安全點(diǎn)時(shí),先更新oopMap,然后進(jìn)行遍歷。然后由RememberedSet輔助進(jìn)行可達(dá)性分析。
這里需要注意的是,雖然有了額外存儲(chǔ)空間的加持,但是整體的存活對(duì)象標(biāo)記耗時(shí)也是不好直接忽視的。
例:某網(wǎng)友的系統(tǒng),因?yàn)椴捎玫恼{(diào)用方式有問(wèn)題,導(dǎo)致系統(tǒng)中存活的業(yè)務(wù)線程高達(dá)幾千之多,那么,要遍歷這么多的OopMap的時(shí)間消耗也是非常大的。
還有一些程序,因?yàn)轭?lèi)似select等查詢語(yǔ)句沒(méi)有添加限制條件,導(dǎo)致大量數(shù)據(jù)加載進(jìn)內(nèi)存,導(dǎo)致GC時(shí)間過(guò)長(zhǎng),甚至是OOM的發(fā)生。
因此,要關(guān)注對(duì)象引用的個(gè)數(shù),包括一個(gè)線程中的引用個(gè)數(shù)和總的線程個(gè)數(shù)。
2.2 本地緩存存放的對(duì)象過(guò)多
我們知道,如果對(duì)象存活的年限達(dá)到了設(shè)置的上限,是會(huì)晉升老年代的。如果有大量的本地緩存對(duì)象被創(chuàng)建,在其晉升到老年代之后,YGC會(huì)通過(guò)掃描card table來(lái)確認(rèn)其是否存活,從而增加YGC的是存活對(duì)象標(biāo)記時(shí)間。
因此,本地緩存方案要評(píng)估完整,或者考慮用堆外緩存減少本地緩存對(duì)GC的影響。
2.3 system class loader 加載對(duì)象過(guò)多
如果程序中有解析xml的邏輯時(shí),每new一個(gè)XStream對(duì)象,就會(huì)新創(chuàng)建一個(gè)classloader,類(lèi)加載器會(huì)和類(lèi)的權(quán)限定名作為key,value為真正的Klass對(duì)象,會(huì)存儲(chǔ)在SystemDirectionary里,最終越來(lái)越多的存活對(duì)象存儲(chǔ)在內(nèi)存里,導(dǎo)致需要占用很長(zhǎng)時(shí)間去標(biāo)記。
“The XStream instance is thread-safe. That is, once the XStream instance has been created and configured, it may be shared across multiple threads allowing objects to be serialized/deserialized concurrently”.
XStream官方的說(shuō)法是XStream線程安全,不需要重復(fù)初始化xstream對(duì)象,為每個(gè)反序列化的對(duì)象聲明一個(gè)靜態(tài)的XStream,重復(fù)利用即可。
2.4JNI & Monitor & finalizable等
暫時(shí)沒(méi)有遇到過(guò)具體的異常實(shí)例,后續(xù)遇到了再補(bǔ)充吧。
3、存活對(duì)象Copy
可能不是特別準(zhǔn)確,因?yàn)闄C(jī)器1的GC不一定會(huì)發(fā)生,只是想用這個(gè)簡(jiǎn)單的例子說(shuō)明下eden區(qū)和新對(duì)象大小對(duì)GC copy的影響,進(jìn)而對(duì)GC時(shí)間產(chǎn)生影響。
所以,要善于運(yùn)用JVM監(jiān)控,預(yù)估每次調(diào)用的新對(duì)象和存活對(duì)象的大小,結(jié)合并發(fā)數(shù),設(shè)計(jì)合理的eden區(qū)和survivor區(qū)。
4、GC日志輸出
GC日志是比較讓人忽略的點(diǎn),但是確實(shí)會(huì)對(duì)GC時(shí)間產(chǎn)生負(fù)面的影響。因?yàn)镚C時(shí)的STW的總時(shí)間內(nèi),包含了GC日志打印的時(shí)間,正常情況下,輸出有限信息的GC日志對(duì)GC整體時(shí)間的影響應(yīng)該是微乎其微的,但是如果正好遇到了GC日志輸出時(shí)系統(tǒng)的IO負(fù)載很好,那么可能會(huì)在日志輸出這里等待很長(zhǎng)的時(shí)間了。
一個(gè)解決辦法是將GC日志文件放到tmpfs上(例如,-Xloggc:/tmpfs/gc.log)。因?yàn)閠mpfs沒(méi)有磁盤(pán)文件備份,所以tmpfs文件不會(huì)導(dǎo)致磁盤(pán)行為,因此也不會(huì)被磁盤(pán)IO阻塞。
reference:后臺(tái)IO異常導(dǎo)致的GC異常 https://www.pianshen.com/article/5926239581/
總結(jié):YGC耗時(shí)問(wèn)題可以說(shuō)千奇百怪,但是萬(wàn)變不離其中,我們只要能夠掌握YGC的幾個(gè)關(guān)鍵節(jié)點(diǎn)涉及的影響,從原理著手分析,應(yīng)該是沒(méi)有什么大問(wèn)題的~
