假笨說-來一道PerfMa面試必考的GC題(肯定漲姿勢)
概述
一般來我們公司面試,我都會習(xí)慣性地問點(diǎn)JVM相關(guān)的問題,當(dāng)然如果他覺得JVM掌握得不錯,我會適當(dāng)多問點(diǎn),畢竟知音難覓,難得在這么一條”狹路上相逢”。
比如今天要說的這個問題,就是我經(jīng)常問的一個問題,只是和我之前排查過的場景有些區(qū)別,屬于另外一種情況。也許我這里講了這個之后,會成為不少公司JVM必問之題,所以本文還是值得大家好好看看的,相信也會讓你很有收獲,我把這個問題簡單歸納為Hotspot GC研發(fā)工程師也許漏掉了一塊邏輯。
昨天美團(tuán)的一個小伙伴在群里問了一個問題,如下圖所示,在上一次YGC之后,from space的使用率是12%,但是在下一次YGC準(zhǔn)備發(fā)生的時候,發(fā)現(xiàn)from space的使用率變成了99%。
OK,看到這里,請停下來思考10秒鐘,想想這個現(xiàn)象是否正常。
- 如果你覺得這個現(xiàn)象不正常,說明你對JVM內(nèi)存分析有一定的理解,但還是沒有完全理解。
- 如果你覺得這個現(xiàn)象沒問題,絕大部分說明你對JVM內(nèi)存分配還不夠熟悉,極少部分情況說明你對它已經(jīng)非常熟悉了,對它實(shí)現(xiàn)上的優(yōu)缺點(diǎn)都了如指掌了。
那請問你是屬于哪種呢?
其實(shí)簡化下來的問題就是:
非GC過程中,新創(chuàng)建的對象可能在from space里分配嗎?
JVM內(nèi)存分配
JVM內(nèi)存分配說簡單也簡單,說復(fù)雜也復(fù)雜,不過我這里不打算說很細(xì),因?yàn)橐堕_講,基本可以講幾個小時,我這里只挑大家熟知的來聊。暫時把大家歸結(jié)為上面的***種情況。
大家知道Java Heap主要由新生代和老生代組成,而新生代又分別由eden+s0(from space)+s1(to space)構(gòu)成,通常情況下s0或者s1有一塊是空的,主要用來做GC copy。
當(dāng)我們創(chuàng)建一個對象的時候,會申請分配一塊內(nèi)存,這塊內(nèi)存主要在新生代里分配,并且是在eden里分配,當(dāng)然某些特殊情況可以直接到老生代去分配,按照這種規(guī)則,正常情況下怎么也輪不到到from space去分配內(nèi)存,因此在上次GC完之后到下次GC之前不可能去from space分配內(nèi)存。
事實(shí)是怎樣呢
那到底是不是這樣呢?從上面的GC日志來看顯然不是這樣,我之前有過一次經(jīng)驗(yàn)是這種情況和GC Locker有關(guān),當(dāng)時在群里要美團(tuán)的同學(xué)把后面的日志發(fā)全一點(diǎn)驗(yàn)證下,結(jié)果比較意外,不是我之前碰到的情況,因此我要了整個完整的GC日志,下面簡單描述下我對這個問題的思考過程。
我拿到GC日志后,***件事就是找到對應(yīng)的GC日志上下文,這種詭異的現(xiàn)象到底是偶爾發(fā)生的還是一直存在,于是我整個日志搜索from space 409600K, 99%,找到***次情況發(fā)生的位置,發(fā)現(xiàn)并不是一開始就有這種情況的,而是到某個時候才開始有,并且全部集中在中間某一段時間里,那我立馬看了下***次發(fā)生的時候的上下文,發(fā)現(xiàn)之前有過一次Full GC和一次CMS GC。
再找了***一次發(fā)生的時候后面的情況
發(fā)現(xiàn)也有次Full GC,那綜合這兩種情況,我基本得出了一個大致的結(jié)論,可能和Full GC有關(guān)。
源碼驗(yàn)證
帶著疑惑我開始找相關(guān)源碼來驗(yàn)證,因?yàn)槲抑烙袕膄rom space分配的情況,于是直接找到了對應(yīng)的方法
從上面的代碼我們可以做一些分析,首先從日志上我們排除了GC Locker的問題,如果是GC Locker,那在JDK8下默認(rèn)會打印出相關(guān)的cause,但是實(shí)際上gc發(fā)生的原因是因?yàn)榉峙涫∷?,于是重點(diǎn)落在了should_allocate_from_space上
- bool should_allocate_from_space() const {
- return _should_allocate_from_space;
- }
那接下來就是找什么地方會設(shè)置這個屬性為true,找到了以下源碼
這是gc發(fā)生之后的一些處理邏輯,并且是full為true的情況,那意味著肯定是Full GC發(fā)生之后才有可能設(shè)置這個屬性set_should_allocate_from_space(),并且也只有在Full GC之后才可能清理這個屬性clear_should_allocate_from_space(),那基本就和我們的現(xiàn)象吻合了。
那是不是所有的Full GC發(fā)生之后都會這樣呢,從上面的代碼來看顯然不是,只有當(dāng)!collection_attempt_is_safe() && !_eden_space->is_empty()為true的時候才會有這種情況,這里我簡單說下可能的場景,當(dāng)我們因?yàn)榉峙鋬?nèi)存不得已發(fā)生了一次Full GC的時候,發(fā)現(xiàn)GC效果不怎么樣,甚至eden里還有對象,老生代也基本是滿的,老生代里的內(nèi)存也不足以容納eden里的對象,此時就會發(fā)生上面的情況。
不過隨著時間的推移,有可能接下來有好轉(zhuǎn),比如做一次CMS GC或許就能把老生代的一些內(nèi)存釋放掉,那其實(shí)整個內(nèi)存就又恢復(fù)了正常,但是這帶來的一個問題就是發(fā)現(xiàn)后面經(jīng)常會發(fā)生從from space里分配內(nèi)存的情況,也就是我們這次碰到的問題,直到下次Full GC發(fā)生之后才會解封,所以我們哪怕執(zhí)行一次jmap -histo:live也足以解封。
GC研發(fā)工程師漏掉的邏輯?
那這樣其實(shí)帶來了一個新的問題,那就是會讓更多的對象盡快晉升到老生代,這會促使老生代GC變得相對比較頻繁,我感覺這其實(shí)應(yīng)該算是JVM的一個bug,或許更應(yīng)該說是GC研發(fā)工程師不小心漏掉了一塊邏輯。
我覺得一個合理的做法是如果后面有CMS GC,那在CMS GC之后,應(yīng)該主動clear_should_allocate_from_space(),也就是在CMS GC的sweep階段執(zhí)行完之后執(zhí)行上面的邏輯,這樣就會有一定保證,事實(shí)上,我們從sweep的源碼里也看到了部分端倪,***調(diào)用了gch->clear_incremental_collection_failed(),所以我個人以為是Hotspot GC開發(fā)的同學(xué)忘記做這件事情了,只需要在gch->clear_incremental_collection_failed()后面調(diào)用新生代的clear_should_allocate_from_space()即可解決此類問題。
結(jié)語
至此應(yīng)該大家知道問題答案了,實(shí)際上是可能在from space里直接分配對象的,但是現(xiàn)在的實(shí)現(xiàn)可能存在一些問題會導(dǎo)致老生代GC變得頻繁。到底這個問題要不要提給OpenJDK社區(qū)呢,不過我的老團(tuán)隊(duì)阿里AJDK,這個問題我覺得應(yīng)該可以修復(fù)一下吧,哈哈哈。
對了,大家如果有GC上的有意思的現(xiàn)象都可以發(fā)郵件給我,我很樂意和大家一起一探究竟。
【本文是51CTO專欄作者李嘉鵬的原創(chuàng)文章,轉(zhuǎn)載請通過微信公眾號(你假笨,id:lovestblog)聯(lián)系作者本人獲取授權(quán)】