面試官問為什么新生代不用標(biāo)記清除算法
本文轉(zhuǎn)載自微信公眾號「安琪拉的博客」,作者安琪拉 。轉(zhuǎn)載本文請聯(lián)系安琪拉的博客公眾號。
杭州某寫字樓,安琪拉穿著新買的19.9的皮鞋走進玻璃隔間辦公室,準(zhǔn)備迎接一場新的表演。
面試官 :看你簡歷上有些熟悉JVM,是吧?
安琪拉:是的
面試官 :那你跟我講講堆內(nèi)存的分區(qū)。
安琪拉:[心想]:這很easy嘛,來,算是回顧一下JVM的基礎(chǔ)知識。
我們知道堆分為新生代和老年代,新生代就是我們說的Yong Generation,老年代是 Old Generation。
面試官 :然后呢?
安琪拉:然后什么?
面試官 :講完啦?下面沒有啦?新生代呢?
安琪拉:你想聽你可以跟我說嘛,你不說我怎么知道你想聽。
新生代又分為Eden區(qū)和Survivor區(qū),Survivor由From區(qū)域和To區(qū)域組成,完整的內(nèi)存結(jié)構(gòu),我給你畫一下,別抽了,筆遞給我一下,我畫一下,如下圖所示。
面試官 :哦,圖可以,那為什么堆要分新生代和老年代呢?
安琪拉:當(dāng)然是為了更有效的管理內(nèi)存。
面試官 :怎么說?
安琪拉:假設(shè)一下,如果不分新老代,內(nèi)存就一整塊,垃圾收集器每次都要把那些長期存在的對象,和生命周期很短的對象放在一起回收,一般長生命周期的對象可能跟應(yīng)用生命周期一致,你基本回收不掉的,比如Spring 框架里面的Bean管理相關(guān)的對象(ApplicationContext),整個應(yīng)用運行期間都存在,這種一般經(jīng)過幾次回收最后都放在老年代,但是如果不區(qū)分新老代,每次都一起回收,性能消耗很大。
區(qū)分新老代之后,老年代放長期存活的對象,新生代就放生命周期短的對象,老年代對象很穩(wěn)定,新生代回收不影響老年代,回收效率能大大提高。
面試官 :那為什么新生代還要分Eden、From、To區(qū)域呢?
安琪拉:[開始慢慢有點意思了]
首先大部分對象生命周期是很短的,如果新生代不分多個區(qū)域,新生代可能會有二種回收方案
第一種可能:每次回收都在新生代整塊內(nèi)存上進行,完整的垃圾回收過程分三步:
需要先找到需要清理的對象標(biāo)記;
清理這些被標(biāo)記的對象;
移動剩下的對象,對達到老年代晉升年齡的對象移動到老年代。
對象被回收掉后會產(chǎn)生很多內(nèi)存碎片(被回收的對象很多),如果要解決內(nèi)存碎片,需要移動剩下的對象(標(biāo)記整理算法),整個回收流程效率很低。
第二種可能:如果沒有Survivor區(qū)(From + To),Minor GC(新生代回收)過程中,存活的對象直接被送到老年代,這樣的話老年代很快被填滿,觸發(fā)Major GC(因為Major GC一般伴隨著Minor GC,也可以看做觸發(fā)了Full GC),F(xiàn)ull GC頻繁會影響程序的執(zhí)行和響應(yīng)速度。
新生代的回收叫Minor GC, 老年代的回收叫Major GC。
面試官 :為什么要設(shè)置兩個Survivor區(qū)呢?From 和 To
安琪拉:我們來看一下, 如果只有一個Survivor區(qū),新生代內(nèi)存的回收流程。
我按照上面這張圖畫的講,第一次Eden區(qū)域滿了,內(nèi)存回收很簡單,直接把Eden區(qū)域存活對象放到Suvivor區(qū)域;
第二次內(nèi)存回收,需要回收二個地方,Eden區(qū)域和Survivor區(qū)域。
- 因為Survivor區(qū)域也會存活的對象需要被回收,對Survivor區(qū)要采用標(biāo)記整理垃圾收集算法,(先標(biāo)記需要清理的對象,然后回收,然后把剩下的存活對象放到一起);
- Eden區(qū)域采用復(fù)制算法,把Eden區(qū)域存放的對象復(fù)制到Survivor區(qū)域,然后把整個Eden區(qū)清除。
看到網(wǎng)上有些文章說這里設(shè)置二個Survivor區(qū)域的原因是為了避免內(nèi)存碎片,因為他假設(shè)第二次(以及后續(xù))的回收,內(nèi)存回收是先回收Eden區(qū)域,然后是Survivor區(qū)域,這樣當(dāng)然會有內(nèi)存碎片,但是如果真是只有一個Survivor區(qū)域,垃圾回收設(shè)計者肯定是先回收Survivor區(qū)域,再回收Eden區(qū)域,等Survivor區(qū)回收整理好,再把Eden區(qū)存放對象搬到Survivor區(qū),這樣存活地址是連續(xù)的,沒有內(nèi)存碎片。所以真正的原因還是我下面說的效率問題。
面試官 :這樣有什么問題呢?
安琪拉:這樣做有幾個問題:
- 經(jīng)過幾次回收之后,Survivor區(qū)域滿了之后怎么辦?直接搬到老年代?那老年代很快就爆炸了。搬到Eden區(qū)?那內(nèi)存碎片產(chǎn)生了,可能Survivor區(qū)和Eden區(qū)回收完之后,還需要再整理一下內(nèi)存去掉內(nèi)存碎片,性能消耗也是很大的。
- 一般標(biāo)記整理算法的性能消耗是比復(fù)制算法消耗要大的,尤其是在新生代98%的對象都是“朝生夕死”的,標(biāo)記清楚的是98%的對象,剩下就2%對象,要整理內(nèi)存,不然直接把這2%對象放到另一個地方,把整塊內(nèi)存清除,Eden整塊內(nèi)存清除效率很高的。
所以歸根結(jié)底,二個Survivor區(qū)還是為了性能考慮,標(biāo)記復(fù)制算法效率比標(biāo)記整理效率高。
面試官 :那你跟我詳細講講標(biāo)記新生代除了Eden,另外采用二個Survivor區(qū)的標(biāo)記復(fù)制算法。
安琪拉:新生代中的對象 98% 是“ 朝生夕死” 的, 所以并不需要按照 1: 1 的比例來劃分Eden和Survivor的空間, 而是將新生代分為較大的一塊Eden空間和兩塊較小的Survivor 空間,每次只使用 Eden 和 其中一塊Survivor[0](From區(qū)域),留出Survivor[1](To區(qū)域)用來實現(xiàn)標(biāo)記復(fù)制。
當(dāng)回收時, 將 Eden 和 Survivor[0] 中還存活著的對象一次性地復(fù)制到另外一塊 Survivor[1] (To)空間上, 最后清理掉 Eden 和 剛才用過的 Survivor 空間。
另外說明一點:From區(qū)域和To區(qū)域在每次Minor GC之后都會互轉(zhuǎn),F(xiàn)rom區(qū)域變成To區(qū)域,To區(qū)域變成From區(qū)域,這只是邏輯標(biāo)識
HotSpot 虛擬機默認(rèn) 將Eden 和 Survivor 的大小比例是 8: 1(CMS不適用), 也就是每次新生代中可用內(nèi)存空間為整個新生代容量的 90%( 80%+ 10%),只有10%的內(nèi)存會被“ 浪費”(一直有10%的內(nèi)存(Survivor To區(qū))不存東西)。
標(biāo)記復(fù)制算法流程:
- Eden區(qū)域+Survivor From區(qū)滿,進行存活對象標(biāo)記,標(biāo)記完,把存活對象復(fù)制到Survivor To區(qū)域;
- Survivor To區(qū)域變成From區(qū)域(一個邏輯標(biāo)識),F(xiàn)rom區(qū)域變成To區(qū)域;
- 內(nèi)存分配,繼續(xù)步驟1,復(fù)制過程中有達到老年代晉升年齡(默認(rèn)值15),移動到老年代。
面試官:剛才說了這么多,是不是來之前背題了?
安琪拉:【心想】回答不出來你說我對技術(shù)沒追求,回答出來了你說我背題,WTF。。
耐心對面試官解釋:怎么可能,我只不過是來之前把安琪拉的博客公眾號上的文章都看了一遍,嘿嘿。
面試官:在哪看,你分享給我。
面試官:誒誒,還有老年代內(nèi)存回收策略呢?還有標(biāo)記整理算法呢?另外講講幾種常見的垃圾回收器,CMS和G1。
安琪拉:不想講了,累了,要不放在二面的時候講吧。
面試官:沒事,二面面試官還是我,你直接講吧。
安琪拉:真不想講了。
面試官:那今天先到這吧,回去等通知,您出了這個門左拐。
文章來源于讀者的提問。