面試官:java的對(duì)象是怎么被回收的?

面試官:我還記得上次你講到JVM內(nèi)存結(jié)構(gòu)(運(yùn)行時(shí)數(shù)據(jù)區(qū)域)提到了「堆」,然后你說是分了幾塊區(qū)域嘛
面試官:當(dāng)時(shí)感覺再講下去那我可能就得加班了
面試官:今天有點(diǎn)空了,繼續(xù)聊聊「堆」那塊吧
候選者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分為「Eden」和「Survivor」區(qū),「survivor」區(qū)又分為「From Survivor」和「To Survivor」區(qū)

候選者:說到這里,我就想聊聊Java的垃圾回收機(jī)制了
面試官:那你開始你的表演吧
候選者:我們使用Java的時(shí)候,會(huì)創(chuàng)建很多對(duì)象,但我們未曾「手動(dòng)」將這些對(duì)象進(jìn)行清除
候選者:而如果用C/C++語言的時(shí)候,用完是需要自己free(釋放)掉的
候選者:那為什么在寫Java的時(shí)候不用我們自己手動(dòng)釋放”垃圾”呢?原因很簡(jiǎn)單,JVM幫我們做了(自動(dòng)回收垃圾)
面試官:嗯…
候選者:我個(gè)人對(duì)垃圾的定義:只要對(duì)象不再被使用了,那我們就認(rèn)為該對(duì)象就是垃圾,對(duì)象所占用的空間就可以被回收

面試官:那是怎么判斷對(duì)象不再被使用的呢?
候選者:常用的算法有兩個(gè)「引用計(jì)數(shù)法」和「可達(dá)性分析法」
候選者:引用計(jì)數(shù)法思路很簡(jiǎn)單:當(dāng)對(duì)象被引用則+1,但對(duì)象引用失敗則-1。當(dāng)計(jì)數(shù)器為0時(shí),說明對(duì)象不再被引用,可以被可回收
候選者:引用計(jì)數(shù)法最明顯的缺點(diǎn)就是:如果對(duì)象存在循環(huán)依賴,那就無法定位該對(duì)象是否應(yīng)該被回收(A依賴B,B依賴A)
面試官:嗯…
候選者:另一種就是可達(dá)性分析法:它從「GC Roots」開始向下搜索,當(dāng)對(duì)象到「GC Roots」都沒有任何引用相連時(shí),說明對(duì)象是不可用的,可以被回收

候選者:「GC Roots」是一組必須「活躍」的引用。從「GC Root」出發(fā),程序通過直接引用或者間接引用,能夠找到可能正在被使用的對(duì)象
面試官:還是不太懂,那「GC Roots」一般是什么?你說它是一組活躍的引用,能不能舉個(gè)例子,太抽象了。
候選者:比如我們上次不是聊到JVM內(nèi)存結(jié)構(gòu)中的虛擬機(jī)棧嗎,虛擬機(jī)棧里不是有棧幀嗎,棧幀不是有局部變量嗎?局部變量不就存儲(chǔ)著引用嘛。
候選者:那如果棧幀位于虛擬機(jī)棧的棧頂,是不是就可以說明這個(gè)棧幀是活躍的(換言之,是線程正在被調(diào)用的)
候選者:既然是線程正在調(diào)用的,那棧幀里的指向「堆」的對(duì)象引用,是不是一定是「活躍」的引用?
候選者:所以,當(dāng)前活躍的棧幀指向堆里的對(duì)象引用就可以是「GC Roots」
面試官:嗯…
候選者:當(dāng)然了,能作為「GC Roots」也不單單只有上面那一小塊
候選者:比如類的靜態(tài)變量引用是「GC Roots」,被「Java本地方法」所引用的對(duì)象也是「GC Roots」等等…
候選者:回到理解的重點(diǎn):「GC Roots」是一組必須「活躍」的「引用」,只要跟「GC Roots」沒有直接或者間接引用相連,那就是垃圾
候選者:JVM用的就是「可達(dá)性分析算法」來判斷對(duì)象是否垃圾
面試官:懂了
候選者:垃圾回收的第一步就是「標(biāo)記」,標(biāo)記哪些沒有被「GC Roots」引用的對(duì)象

候選者:標(biāo)記完之后,我們就可以選擇直接「清除」,只要不被「GC Roots」關(guān)聯(lián)的,都可以干掉
候選者:過程非常簡(jiǎn)單粗暴,但也存在很明顯的問題
候選者:直接清除會(huì)有「內(nèi)存碎片」的問題:可能我有10M的空余內(nèi)存,但程序申請(qǐng)9M內(nèi)存空間卻申請(qǐng)不下來(10M的內(nèi)存空間是垃圾清除后的,不連續(xù)的)

候選者:那解決「內(nèi)存碎片」的問題也比較簡(jiǎn)單粗暴,「標(biāo)記」完,不直接「清除」。
候選者:我把「標(biāo)記」存活的對(duì)象「復(fù)制」到另一塊空間,復(fù)制完了之后,直接把原有的整塊空間給干掉!這樣就沒有內(nèi)存碎片的問題了
候選者:這種做法缺點(diǎn)又很明顯:內(nèi)存利用率低,得有一塊新的區(qū)域給我復(fù)制(移動(dòng))過去
面試官:嗯…
候選者:還有一種「折中」的辦法,我未必要有一塊「大的完整空間」才能解決內(nèi)存碎片的問題,我只要能在「當(dāng)前區(qū)域」內(nèi)進(jìn)行移動(dòng)
候選者:把存活的對(duì)象移到一邊,把垃圾移到一邊,那再將垃圾一起刪除掉,不就沒有內(nèi)存碎片了嘛
候選者:這種專業(yè)的術(shù)語就叫做「整理」

候選者:扯了這么久,我們把思維再次回到「堆」中吧
候選者:經(jīng)過研究表明:大部分對(duì)象的生命周期都很短,而只有少部分對(duì)象可能會(huì)存活很長(zhǎng)時(shí)間
候選者:又由于「垃圾回收」是會(huì)導(dǎo)致「stop the world」(應(yīng)用停止訪問)
候選者:理解「stop the world」應(yīng)該很簡(jiǎn)單吧:回收垃圾的時(shí)候,程序是有短暫的時(shí)間不能正常繼續(xù)運(yùn)作啊。不然JVM在回收的時(shí)候,用戶線程還繼續(xù)分配修改引用,JVM怎么搞(:
候選者:為了使「stop the world」持續(xù)的時(shí)間盡可能短以及提高并發(fā)式GC所能應(yīng)付的內(nèi)存分配速率
候選者:在很多的垃圾收集器上都會(huì)在「物理」或者「邏輯」上,把這兩類對(duì)象進(jìn)行區(qū)分,死得快的對(duì)象所占的區(qū)域叫做「年輕代」,活得久的對(duì)象所占的區(qū)域叫做「老年代」

候選者:但也不是所有的「垃圾收集器」都會(huì)有,只不過我們現(xiàn)在線上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有「分代」概念的。
候選者:所以,你可以看到我的「堆」是畫了「年輕代」和「老年代」
候選者:要值得注意的是,高版本所使用的垃圾收集器的ZGC是沒有分代的概念的(:
候選者:只不過我為了好說明現(xiàn)狀,ZGC的話有空我們?cè)倭?/p>
面試官:嗯…好吧
候選者:在前面更前面提到了垃圾回收的過程,其實(shí)就對(duì)應(yīng)著幾種「垃圾回收算法」,分別是:
候選者:標(biāo)記清除算法、標(biāo)記復(fù)制算法和標(biāo)記整理算法【「標(biāo)記」「清除」「復(fù)制」「整理」】
候選者:經(jīng)過上面的鋪墊之后,這幾種算法應(yīng)該還是比較好理解的

候選者:「分代」和「垃圾回收算法」都搞明白了之后,我們就可以看下在JDK8生產(chǎn)環(huán)境及以下常見的垃圾回收器了
候選者:「年輕代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew
候選者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS
候選者:看著垃圾收集器有很多,其實(shí)還是非常好理解的。Serial是單線程的,Parallel是多線程
候選者:這些垃圾收集器實(shí)際上就是「實(shí)現(xiàn)了」垃圾回收算法(標(biāo)記復(fù)制、標(biāo)記整理以及標(biāo)記清除算法)
候選者:CMS是「JDK8之前」是比較新的垃圾收集器,它的特點(diǎn)是能夠盡可能減少「stop the world」時(shí)間。在垃圾回收時(shí)讓用戶線程和 GC 線程能夠并發(fā)執(zhí)行!

候選者:又可以發(fā)現(xiàn)的是,「年輕代」的垃圾收集器使用的都是「標(biāo)記復(fù)制算法」
候選者:所以在「堆內(nèi)存」劃分中,將年輕代劃分出Survivor區(qū)(Survivor From 和Survivor To),目的就是為了有一塊完整的內(nèi)存空間供垃圾回收器進(jìn)行拷貝(移動(dòng))
候選者:而新的對(duì)象則放入Eden區(qū)
候選者:我下面重新畫下「堆內(nèi)存」的圖,因?yàn)樗鼈兊拇笮∈怯心J(rèn)的比例的

候選者:圖我已經(jīng)畫好了,應(yīng)該就不用我再說明了
面試官:我還想問問,就是,新創(chuàng)建的對(duì)象一般是在「新生代」嘛,那在什么時(shí)候會(huì)到「老年代」中呢?
候選者:嗯,我認(rèn)為簡(jiǎn)單可以分為兩種情況:
候選者:1. 如果對(duì)象太大了,就會(huì)直接進(jìn)入老年代(對(duì)象創(chuàng)建時(shí)就很大 || Survivor區(qū)沒辦法存下該對(duì)象)
候選者:2. 如果對(duì)象太老了,那就會(huì)晉升至老年代(每發(fā)生一次Minor GC ,存活的對(duì)象年齡+1,達(dá)到默認(rèn)值15則晉升老年代 || 動(dòng)態(tài)對(duì)象年齡判定 可以進(jìn)入老年代)

面試官:既然你又提到了Minor GC,那Minor GC 什么時(shí)候會(huì)觸發(fā)呢?
候選者:當(dāng)Eden區(qū)空間不足時(shí),就會(huì)觸發(fā)Minor GC
面試官:Minor GC 在我的理解就是「年輕代」的GC,你前面又提到了「GC Roots」嘛
面試官:那在「年輕代」GC的時(shí)候,從GC Roots出發(fā),那不也會(huì)掃描到「老年代」的對(duì)象嗎?那那那..不就相當(dāng)于全堆掃描嗎?
候選者:這JVM里也有解決辦法的。
候選者:HotSpot 虛擬機(jī)「老的GC」(G1以下)是要求整個(gè)GC堆在連續(xù)的地址空間上。
候選者:所以會(huì)有一條分界線(一側(cè)是老年代,另一側(cè)是年輕代),所以可以通過「地址」就可以判斷對(duì)象在哪個(gè)分代上
候選者:當(dāng)做Minor GC的時(shí)候,從GC Roots出發(fā),如果發(fā)現(xiàn)「老年代」的對(duì)象,那就不往下走了(Minor GC對(duì)老年代的區(qū)域毫無興趣)

面試官:但又有個(gè)問題,那如果「年輕代」的對(duì)象被「老年代」引用了呢?(老年代對(duì)象持有年輕代對(duì)象的引用),那時(shí)候肯定是不能回收掉「年輕代」的對(duì)象的。
候選者:HotSpot虛擬機(jī)下 有「card table」(卡表)來避免全局掃描「老年代」對(duì)象
候選者:「堆內(nèi)存」的每一小塊區(qū)域形成「卡頁」,卡表實(shí)際上就是卡頁的集合。當(dāng)判斷一個(gè)卡頁中有存在對(duì)象的跨代引用時(shí),將這個(gè)頁標(biāo)記為「臟頁」
候選者:那知道了「卡表」之后,就很好辦了。每次Minor GC 的時(shí)候只需要去「卡表」找到「臟頁」,找到后加入至GC Root,而不用去遍歷整個(gè)「老年代」的對(duì)象了。

面試官:嗯嗯嗯,還可以的啊,要不繼續(xù)聊聊CMS?
候選者:這面試快一個(gè)小時(shí)了吧,我圖也畫了這么多了。下次?下次吧?有點(diǎn)兒累了
本文總結(jié):
- 什么是垃圾:只要對(duì)象不再被使用,那即是垃圾
- 如何判斷為垃圾:可達(dá)性分析算法和引用計(jì)算算法,JVM使用的是可達(dá)性分析算法
- 什么是GC Roots:GC Roots是一組必須活躍的引用,跟GC Roots無關(guān)聯(lián)的引用即是垃圾,可被回收
- 常見的垃圾回收算法:標(biāo)記清除、標(biāo)記復(fù)制、標(biāo)記整理
- 為什么需要分代:大部分對(duì)象都死得早,只有少部分對(duì)象會(huì)存活很長(zhǎng)時(shí)間。在堆內(nèi)存上都會(huì)在物理或邏輯上進(jìn)行分代,為了使「stop the world」持續(xù)的時(shí)間盡可能短以及提高并發(fā)式GC所能應(yīng)付的內(nèi)存分配速率。
- Minor GC:當(dāng)Eden區(qū)滿了則觸發(fā),從GC Roots往下遍歷,年輕代GC不關(guān)心老年代對(duì)象
- 什么是card table【卡表】:空間換時(shí)間(類似bitmap),能夠避免掃描老年代的所有對(duì)應(yīng)進(jìn)而順利進(jìn)行Minor GC (案例:老年代對(duì)象持有年輕代對(duì)象引用)
- 堆內(nèi)存占比:年輕代占堆內(nèi)存1/3,老年代占堆內(nèi)存2/3。Eden區(qū)占年輕代8/10,Survivor區(qū)占年輕代2/10(其中From 和To 各站1/10)

對(duì)線面試官PDF版本,可+V: java3yyy 免費(fèi)領(lǐng)取