BAT面試必問題系列:JVM判斷物件是否已死和四種垃圾回收演算法總結
JVM系列:
面試題一:判斷物件是否已死
判斷物件是否已死就是找出哪些物件是已經死掉的,以後不會再用到的,就像地上有廢紙、飲料瓶和百元大鈔,掃地前要先判斷出地上廢紙和飲料瓶是垃圾,百元大鈔不是垃圾。判斷物件是否已死有引用計數演算法和可達性分析演算法。
1.引用計數演算法
給每一個物件新增一個引用計數器,每當有一個地方引用它時,計數器值加 1;每當有一個地方不再引用它時,計數器值減 1,這樣只要計數器的值不為 0,就說明還有地方引用它,它就不是無用的物件。如下圖,物件 2 有 1 個引用,它的引用計數器值為 1,物件 1有兩個地方引用,它的引用計數器值為 2 。
這種方法看起來非常簡單,但目前許多主流的虛擬機器都沒有選用這種演算法來管理記憶體,原因就是當某些物件之間互相引用時,無法判斷出這些物件是否已死,如下圖,物件 1 和物件 2 都沒有被堆外的變數引用,而是被對方互相引用,這時他們雖然沒有用處了,但是引用計數器的值仍然是 1,無法判斷他們是死物件,垃圾回收器也就無法回收。
2.可達性分析演算法
瞭解可達性分析演算法之前先了解一個概念——GC Roots,垃圾收集的起點,可以作為 GC Roots 的有虛擬機器棧中本地變量表中引用的物件、方法區中靜態屬性引用的物件、方法區中常量引用的物件、本地方法棧中 JNI(Native 方法)引用的物件。
當一個物件到 GC Roots 沒有任何引用鏈相連(GC Roots 到這個物件不可達)時,就說明此物件是不可用的,是死物件。
如下圖:object1、object2、object3、object4 和 GC Roots 之間有可達路徑,這些物件不會被回收,但 object5、object6、object7 到 GC Roots 之間沒有可達路徑,這些物件就被判了死刑。
上面被判了死刑的物件(object5、object6、object7)並不是必死無疑,還有挽救的餘地。進行可達性分析後物件和 GC Roots 之間沒有引用鏈相連時,物件將會被進行一次標記,接著會判斷如果物件沒有覆蓋 Object的finalize() 方法或者 finalize() 方法已經被虛擬機器呼叫過,那麼它們就會被行刑(清除);如果物件覆蓋了 finalize() 方法且還沒有被呼叫,則會執行 finalize() 方法中的內容,所以在 finalize() 方法中如果重新與 GC Roots 引用鏈上的物件關聯就可以拯救自己,但是一般不建議這麼做,周志明老師也建議大家完全可以忘掉這個方法~
3.方法區回收
上面說的都是對堆記憶體中物件的判斷,方法區中主要回收的是廢棄的常量和無用的類。
判斷常量是否廢棄可以判斷是否有地方引用這個常量,如果沒有引用則為廢棄的常量。
判斷類是否廢棄需要同時滿足如下條件:
該類所有的例項已經被回收(堆中不存在任何該類的例項)。
載入該類的 ClassLoader 已經被回收。
該類對應的 java.lang.Class 物件在任何地方沒有被引用(無法通過反射訪問該類的方法)。
面試題二:常用四種垃圾回收演算法
常用的垃圾回收演算法有四種:標記-清除演算法、複製演算法、標記-整理演算法、分代收集演算法。
1.標記-清除演算法
分為標記和清除兩個階段,首先標記出所有需要回收的物件,標記完成後統一回收所有被標記的物件,如下圖。
缺點:標記和清除兩個過程效率都不高;標記清除之後會產生大量不連續的記憶體碎片。
2.複製演算法
把記憶體分為大小相等的兩塊,每次儲存只用其中一塊,當這一塊用完了,就把存活的物件全部複製到另一塊上,同時把使用過的這塊記憶體空間全部清理掉,往復迴圈,如下圖。
缺點:實際可使用的記憶體空間縮小為原來的一半,比較適合。
3.標記-整理演算法
先對可用的物件進行標記,然後所有被標記的物件向一段移動,最後清除可用物件邊界以外的記憶體,如下圖。
4.分代收集演算法
把堆記憶體分為新生代和老年代,新生代又分為 Eden 區、From Survivor 和 To Survivor。一般新生代中的物件基本上都是朝生夕滅的,每次只有少量物件存活,因此採用複製演算法,只需要複製那些少量存活的物件就可以完成垃圾收集;老年代中的物件存活率較高,就採用標記-清除和標記-整理演算法來進行回收。
在這些區域的垃圾回收大概有如下幾種情況:
大多數情況下,新的物件都分配在Eden區,當 Eden 區沒有空間進行分配時,將進行一次 Minor GC,清理 Eden 區中的無用物件。清理後,Eden 和 From Survivor 中的存活物件如果小於To Survivor 的可用空間則進入To Survivor,否則直接進入老年代);Eden 和 From Survivor 中還存活且能夠進入 To Survivor 的物件年齡增加 1 歲(虛擬機器為每個物件定義了一個年齡計數器,每執行一次 Minor GC 年齡加 1),當存活物件的年齡到達一定程度(預設 15 歲)後進入老年代,可以通過 -XX:MaxTenuringThreshold 來設定年齡的值。
當進行了 Minor GC 後,Eden 還不足以為新物件分配空間(那這個新物件肯定很大),新物件直接進入老年代。
佔 To Survivor 空間一半以上且年齡相等的物件,大於等於該年齡的物件直接進入老年代,比如 Survivor 空間是 10M,有幾個年齡為 4 的物件佔用總空間已經超過 5M,則年齡大於等於 4 的物件都直接進入老年代,不需要等到 MaxTenuringThreshold 指定的歲數。
在進行 Minor GC 之前,會判斷老年代最大連續可用空間是否大於新生代所有物件總空間,如果大於,說明 Minor GC 是安全的,否則會判斷是否允許擔保失敗,如果允許,判斷老年代最大連續可用空間是否大於歷次晉升到老年代的物件的平均大小,如果大於,則執行 Minor GC,否則執行 Full GC。
當在 java 程式碼裡直接呼叫 System.gc() 時,會建議 JVM 進行 Full GC,但一般情況下都會觸發 Full GC,一般不建議使用,儘量讓虛擬機器自己管理 GC 的策略。
永久代(方法區)中用於存放類資訊,jdk1.6 及之前的版本永久代中還儲存常量、靜態變數等,當永久代的空間不足時,也會觸發 Full GC,如果經過 Full GC 還無法滿足永久代存放新資料的需求,就會丟擲永久代的記憶體溢位異常。
大物件(需要大量連續記憶體的物件)例如很長的陣列,會直接進入老年代,如果老年代沒有足夠的連續大空間來存放,則會進行 Full