面試官:我還記得上次你講到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)
第一時間獲取BATJTMD一線互聯網大廠最新的面試資料以及內推機會關注公眾號「對線面試官」

歡迎關注我的微信公眾號【Java3y】來聊聊Java面試,對線面試官系列持續更新中!

【對線面試官-移動端】系列 一周兩篇持續更新中!
【對線面試官-電腦端】系列 一周兩篇持續更新中!
原創不易!!求三連!!