垃圾回收算法基礎
翻譯原文 => plumbr Java GC handbook
前文參見:
在深入GC算法的實現細節之前,我們最好先來了解下相關術語及背后的基本原理。不同回收器的實現細節各有不同,但總的來說基本所有的回收器都會關注如下兩個方面:
- 找出所有的存活對象
- 清理掉所有的其它對象——也就是那些被認為是廢棄或無用的對象。
首先,所有回收器都會通過一個標記過程來對存活對象進行統計。
標記可達對象
JVM中用到的所有現代GC算法在回收前都會先找出所有仍存活的對象。下圖中所展示的JVM中的內存布局可以用來很好地闡釋這一概念:
首先,垃圾回收器將某些特殊的對象定義為GC根對象。所謂的GC根對象包括:
- 當前執行方法中的所有本地變量及入參
- 活躍線程
- 已加載類中的靜態變量
- JNI引用
接下來,垃圾回收器會對內存中的整個對象圖進行遍歷,它先從GC根對象開始,然后是根對象引用的其它對象,比如實例變量。回收器將訪問到的所有對象都標記為存活。
存活對象在上圖中被標記為藍色。當標記階段完成了之后,所有的存活對象都已經被標記完了。其它的那些(上圖中灰色的那些)也就是GC根對象不可達的對象,也就是說你的應用不會再用到它們了。這些就是垃圾對象,回收器將會在接下來的階段中清除它們。
關于標記階段有幾個關鍵點是值得注意的:
- 開始進行標記前,需要先暫停應用線程,否則如果對象圖一直在變化的話是無法真正去遍歷它的。暫停應用線程以便JVM可以盡情地進行整理記錄的程序位置點又被稱之為安全點(Safe Point),這會觸發一次Stop The World(STW)暫停。觸發安全點的原因有許多,但最常見的應該就是垃圾回收了。
- 暫停時間的長短并不取決于堆內對象的多少也不是堆的大小,而是存活對象的多少。因此,調高堆的大小并不會影響到標記階段的時間長短。
當標記階段完成后,GC開始進入下一階段,刪除不可達對象。
刪除無用對象
不同的GC算法在刪除無用對象上的做法會有所不同,不過大致上可以為分三類:清除(Sweeping),整理/壓縮(Compacting)以及拷貝(Copying)。下面的幾節將會詳細介紹下這幾種算法的不同。
清除
從概念上來講,標記-清除算法使用的方法是最簡單的,只需要忽略這些對象便可以了。也就是說當標記階段完成之后,未被訪問到的對象所在的空間都會被認為是空閑的,可以用來創建新的對象。
這種方法需要使用一個空閑列表來記錄所有的空閑區域以及大小。對空閑列表的管理會增加分配對象時的工作量。這種方法還有一個缺陷就是——雖然空閑區域的大小是足夠的,但卻可能沒有一個單一區域能夠滿足這次分配所需的大小,因此本次分配還是會失敗(在Java中就是一次OutOfMemoryError)。
整理
標記-清除-整理算法修復了標記-清除算法的短板——它將所有標記的也就是存活的對象都移動到內存區域的開始位置。這種方法的缺點就是GC暫停的時間會增長,因為你需要將所有的對象都拷貝到一個新的地方,還得更新它們的引用地址。相對于標記-清除算法,它的優點也是顯而易見的——經過整理之后,新對象的分配只需要通過指針碰撞便能完成(pointer bumping),相當簡單。使用這種方法空閑區域的位置是始終可知的,也不會再有碎片的問題了。
復制
標記-復制算法與標記-整理算法非常類似,它們都會將所有存活對象重新進行分配。區別在于重新分配的目標地址不同,復制算法是為存活對象分配了另外的內存區域作為它們的新家。標記復制算法的優點在于標記階段和復制階段可以同時進行。它的缺點是需要一塊能容納下所有存活對象的額外的內存空間。