面試官:我還記得上次你講到JVM記憶體結構(執行時資料區域)提到了「堆」,然後你說是分了幾塊區域嘛
面試官:當時感覺再講下去那我可能就得加班了
面試官:今天有點空了,繼續聊聊「堆」那塊吧
候選者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分為「Eden」和「Survivor」區,「survivor」區又分為「From Survivor」和「To Survivor」區
候選者:說到這裡,我就想聊聊Java的垃圾回收機制了
面試官:那你開始你的表演吧
候選者:我們使用Java的時候,會建立很多物件,但我們未曾「手動」將這些物件進行清除
候選者:而如果用C/C++語言的時候,用完是需要自己free(釋放)掉的
候選者:那為什麼在寫Java的時候不用我們自己手動釋放”垃圾”呢?原因很簡單,JVM幫我們做了(自動回收垃圾)
面試官:嗯…
候選者:我個人對垃圾的定義:只要物件不再被使用了,那我們就認為該物件就是垃圾,物件所佔用的空間就可以被回收
面試官:那是怎麼判斷物件不再被使用的呢?
候選者:常用的演算法有兩個「引用計數法」和「可達性分析法」
候選者:引用計數法思路很簡單:當物件被引用則+1,但物件引用失敗則-1。當計數器為0時,說明物件不再被引用,可以被可回收
候選者:引用計數法最明顯的缺點就是:如果物件存在迴圈依賴,那就無法定位該物件是否應該被回收(A依賴B,B依賴A)
面試官:嗯…
候選者:另一種就是可達性分析法:它從「GC Roots」開始向下搜尋,當物件到「GC Roots」都沒有任何引用相連時,說明物件是不可用的,可以被回收
候選者:「GC Roots」是一組必須「活躍」的引用。從「GC Root」出發,程式通過直接引用或者間接引用,能夠找到可能正在被使用的物件
面試官:還是不太懂,那「GC Roots」一般是什麼?你說它是一組活躍的引用,能不能舉個例子,太抽象了。
候選者:比如我們上次不是聊到JVM記憶體結構中的虛擬機器棧嗎,虛擬機器棧裡不是有棧幀嗎,棧幀不是有區域性變數嗎?區域性變數不就儲存著引用嘛。
候選者:那如果棧幀位於虛擬機器棧的棧頂,是不是就可以說明這個棧幀是活躍的(換言之,是執行緒正在被呼叫的)
候選者:既然是執行緒正在呼叫的,那棧幀裡的指向「堆」的物件引用,是不是一定是「活躍」的引用?
候選者:所以,當前活躍的棧幀指向堆裡的物件引用就可以是「GC Roots」
面試官:嗯…
候選者:當然了,能作為「GC Roots」也不單單隻有上面那一小塊
候選者:比如類的靜態變數引用是「GC Roots」,被「Java本地方法」所引用的物件也是「GC Roots」等等…
候選者:回到理解的重點:「GC Roots」是一組必須「活躍」的「引用」,只要跟「GC Roots」沒有直接或者間接引用相連,那就是垃圾
候選者:JVM用的就是「可達性分析演算法」來判斷物件是否垃圾
面試官:懂了
候選者:垃圾回收的第一步就是「標記」,標記哪些沒有被「GC Roots」引用的物件
候選者:標記完之後,我們就可以選擇直接「清除」,只要不被「GC Roots」關聯的,都可以幹掉
候選者:過程非常簡單粗暴,但也存在很明顯的問題
候選者:直接清除會有「記憶體碎片」的問題:可能我有10M的空餘記憶體,但程式申請9M記憶體空間卻申請不下來(10M的記憶體空間是垃圾清除後的,不連續的)
候選者:那解決「記憶體碎片」的問題也比較簡單粗暴,「標記」完,不直接「清除」。
候選者:我把「標記」存活的物件「複製」到另一塊空間,複製完了之後,直接把原有的整塊空間給幹掉!這樣就沒有記憶體碎片的問題了
候選者:這種做法缺點又很明顯:記憶體利用率低,得有一塊新的區域給我複製(移動)過去
面試官:嗯…
候選者:還有一種「折中」的辦法,我未必要有一塊「大的完整空間」才能解決記憶體碎片的問題,我只要能在「當前區域」內進行移動
候選者:把存活的物件移到一邊,把垃圾移到一邊,那再將垃圾一起刪除掉,不就沒有記憶體碎片了嘛
候選者:這種專業的術語就叫做「整理」
候選者:扯了這麼久,我們把思維再次回到「堆」中吧
候選者:經過研究表明:大部分物件的生命週期都很短,而只有少部分物件可能會存活很長時間
候選者:又由於「垃圾回收」是會導致「stop the world」(應用停止訪問)
候選者:理解「stop the world」應該很簡單吧:回收垃圾的時候,程式是有短暫的時間不能正常繼續運作啊。不然JVM在回收的時候,使用者執行緒還繼續分配修改引用,JVM怎麼搞(:
候選者:為了使「stop the world」持續的時間儘可能短以及提高併發式GC所能應付的記憶體分配速率
候選者:在很多的垃圾收集器上都會在「物理」或者「邏輯」上,把這兩類物件進行區分,死得快的物件所佔的區域叫做「年輕代」,活得久的物件所佔的區域叫做「老年代」
候選者:但也不是所有的「垃圾收集器」都會有,只不過我們現線上上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有「分代」概念的。
候選者:所以,你可以看到我的「堆」是畫了「年輕代」和「老年代」
候選者:要值得注意的是,高版本所使用的垃圾收集器的ZGC是沒有分代的概念的(:
候選者:只不過我為了好說明現狀,ZGC的話有空我們再聊
面試官:嗯…好吧
候選者:在前面更前面提到了垃圾回收的過程,其實就對應著幾種「垃圾回收演算法」,分別是:
候選者:標記清除演算法、標記複製演算法和標記整理演算法【「標記」「清除」「複製」「整理」】
候選者:經過上面的鋪墊之後,這幾種演算法應該還是比較好理解的
候選者:「分代」和「垃圾回收演算法」都搞明白了之後,我們就可以看下在JDK8生產環境及以下常見的垃圾回收器了
候選者:「年輕代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew
候選者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS
候選者:看著垃圾收集器有很多,其實還是非常好理解的。Serial是單執行緒的,Parallel是多執行緒
候選者:這些垃圾收集器實際上就是「實現了」垃圾回收演算法(標記複製、標記整理以及標記清除演算法)
候選者:CMS是「JDK8之前」是比較新的垃圾收集器,它的特點是能夠儘可能減少「stop the world」時間。在垃圾回收時讓使用者執行緒和 GC 執行緒能夠併發執行!
候選者:又可以發現的是,「年輕代」的垃圾收集器使用的都是「標記複製演算法」
候選者:所以在「堆記憶體」劃分中,將年輕代劃分出Survivor區(Survivor From 和Survivor To),目的就是為了有一塊完整的記憶體空間供垃圾回收器進行拷貝(移動)
候選者:而新的物件則放入Eden區
候選者:我下面重新畫下「堆記憶體」的圖,因為它們的大小是有預設的比例的
候選者:圖我已經畫好了,應該就不用我再說明了
面試官:我還想問問,就是,新建立的物件一般是在「新生代」嘛,那在什麼時候會到「老年代」中呢?
候選者:嗯,我認為簡單可以分為兩種情況:
候選者:1. 如果物件太大了,就會直接進入老年代(物件建立時就很大 || Survivor區沒辦法存下該物件)
候選者:2. 如果物件太老了,那就會晉升至老年代(每發生一次Minor GC ,存活的物件年齡+1,達到預設值15則晉升老年代 || 動態物件年齡判定 可以進入老年代)
面試官:既然你又提到了Minor GC,那Minor GC 什麼時候會觸發呢?
候選者:當Eden區空間不足時,就會觸發Minor GC
面試官:Minor GC 在我的理解就是「年輕代」的GC,你前面又提到了「GC Roots」嘛
面試官:那在「年輕代」GC的時候,從GC Roots出發,那不也會掃描到「老年代」的物件嗎?那那那..不就相當於全堆掃描嗎?
候選者:這JVM裡也有解決辦法的。
候選者:HotSpot 虛擬機器「老的GC」(G1以下)是要求整個GC堆在連續的地址空間上。
候選者:所以會有一條分界線(一側是老年代,另一側是年輕代),所以可以通過「地址」就可以判斷物件在哪個分代上
候選者:當做Minor GC的時候,從GC Roots出發,如果發現「老年代」的物件,那就不往下走了(Minor GC對老年代的區域毫無興趣)
面試官:但又有個問題,那如果「年輕代」的物件被「老年代」引用了呢?(老年代物件持有年輕代物件的引用),那時候肯定是不能回收掉「年輕代」的物件的。
候選者:HotSpot虛擬機器下 有「card table」(卡表)來避免全域性掃描「老年代」物件
候選者:「堆記憶體」的每一小塊區域形成「卡頁」,卡表實際上就是卡頁的集合。當判斷一個卡頁中有存在物件的跨代引用時,將這個頁標記為「髒頁」
候選者:那知道了「卡表」之後,就很好辦了。每次Minor GC 的時候只需要去「卡表」找到「髒頁」,找到後加入至GC Root,而不用去遍歷整個「老年代」的物件了。
面試官:嗯嗯嗯,還可以的啊,要不繼續聊聊CMS?
候選者:這面試快一個小時了吧,我圖也畫了這麼多了。下次?下次吧?有點兒累了
本文總結:
- 什麼是垃圾:只要物件不再被使用,那即是垃圾
- 如何判斷為垃圾:可達性分析演算法和引用計算演算法,JVM使用的是可達性分析演算法
- 什麼是GC Roots:GC Roots是一組必須活躍的引用,跟GC Roots無關聯的引用即是垃圾,可被回收
- 常見的垃圾回收演算法:標記清除、標記複製、標記整理
- 為什麼需要分代:大部分物件都死得早,只有少部分物件會存活很長時間。在堆記憶體上都會在物理或邏輯上進行分代,為了使「stop the world」持續的時間儘可能短以及提高併發式GC所能應付的記憶體分配速率。
- Minor GC:當Eden區滿了則觸發,從GC Roots往下遍歷,年輕代GC不關心老年代物件
- 什麼是card table【卡表】:空間換時間(類似bitmap),能夠避免掃描老年代的所有對應進而順利進行Minor GC (案例:老年代物件持有年輕代物件引用)
- 堆記憶體佔比:年輕代佔堆記憶體1/3,老年代佔堆記憶體2/3。Eden區佔年輕代8/10,Survivor區佔年輕代2/10(其中From 和To 各站1/10)
歡迎關注我的微信公眾號【Java3y】來聊聊Java面試,對線面試官系列持續更新中!
【對線面試官-移動端】系列 一週兩篇持續更新中!
【對線面試官-電腦端】系列 一週兩篇持續更新中!
原創不易!!求三連!!