JVM--標記-清除演算法Mark-Sweep
前言
垃圾自動回收機制的出現使程式設計更加的簡單,使得我們不需要再去考慮記憶體分配和釋放的問題,而是更加的專注在我們產品功能的實現上。但是我們還是需要花時間去了解下垃圾收集機制是怎麼工作的,以便後面能夠更好的進行我們應用的效能調優等。
目前最基本的垃圾收集演算法有四種,標記-清除演算法(mark-sweep),標記-壓縮演算法(mark-compact),複製演算法(copying)以及引用計數演算法(reference counting).而現代流行的垃圾收集演算法一般是由這四種中的其中幾種演算法相互組合而成,比如說,對堆(heap)的一部分採用標記-清除演算法,對堆(heap)的另外一部分則採用複製演算法等等。今天我們主要來看下標記-清除演算法的原理。
基本概念
在瞭解標記-清除演算法前,我們先要了解幾個基本概念。
1.首先是mutator和collector,這兩個名詞經常在垃圾收集演算法中出現,collector指的就是垃圾收集器,而mutator是指除了垃圾收集器之外的部分,比如說我們應用程式本身。mutator的職責一般是NEW(分配記憶體),READ(從記憶體中讀取內容),WRITE(將內容寫入記憶體),而collector則就是回收不再使用的記憶體來供mutator進行NEW操作的使用。
2. 第二個基本概念是關於mutator roots(mutator根物件),mutator根物件一般指的是分配在堆記憶體之外,可以直接被mutator直接訪問到的物件,一般是指靜態/全域性變數以及Thread-Local變數(在Java中,儲存在java.lang.ThreadLocal中的變數和分配在棧上的變數 - 方法內部的臨時變數等都屬於此類).
3.第三個基本概念是關於可達物件的定義,從mutator根物件開始進行遍歷,可以被訪問到的物件都稱為是可達物件。這些物件也是mutator(你的應用程式)正在使用的物件。
演算法原理
顧名思義,標記-清除演算法分為兩個階段,標記(mark)和清除(sweep).
在標記階段,collector從mutator根物件開始進行遍歷,對從mutator根物件可以訪問到的物件都打上一個標識,一般是在物件的header中,將其記錄為可達物件。
而在清除階段,collector對堆記憶體(heap memory)從頭到尾進行線性的遍歷,如果發現某個物件沒有標記為可達物件-通過讀取物件的header資訊,則就將其回收。
從上圖我們可以看到,在Mark階段,從根物件1可以訪問到B物件,從B物件又可以訪問到E物件,所以B,E物件都是可達的。同理,F,G,J,K也都是可達物件。到了Sweep階段,所有非可達物件都會被collector回收。同時,Collector在進行標記和清除階段時會將整個應用程式暫停(mutator),等待標記清除結束後才會恢復應用程式的執行,這也是Stop-The-World這個單詞的來歷。
接著我們先看下一般垃圾收集動作是怎麼被觸發的,下面是mutator進行NEW操作的虛擬碼:
New():
ref <- allocate() //分配新的記憶體到ref指標
if ref == null
collect() //記憶體不足,則觸發垃圾收集
ref <- allocate()
if ref == null
throw "Out of Memory" //垃圾收集後仍然記憶體不足,則丟擲Out of Memory錯誤
return ref
atomic collect():
markFromRoots()
sweep(HeapStart,HeapEnd)
而下面是對應的mark演算法:
markFromRoots():
worklist <- empty
for each fld in Roots //遍歷所有mutator根物件
ref <- *fld
if ref != null && isNotMarked(ref) //如果它是可達的而且沒有被標記的,直接標記該物件並將其加到worklist中
setMarked(ref)
add(worklist,ref)
mark()
mark():
while not isEmpty(worklist)
ref <- remove(worklist) //將worklist的最後一個元素彈出,賦值給ref
for each fld in Pointers(ref) //遍歷ref物件的所有指標域,如果其指標域(child)是可達的,直接標記其為可達物件並且將其加入worklist中
//通過這樣的方式來實現深度遍歷,直到將該物件下面所有可以訪問到的物件都標記為可達物件。
child <- *fld
if child != null && isNotMarked(child)
setMarked(child)
add(worklist,child)
在mark階段結束後,sweep演算法就比較簡單了,它就是從堆記憶體起始位置開始,線性遍歷所有物件直到堆記憶體末尾,如果該物件是可達物件的(在mark階段被標記過的),那就直接去除標記位(為下一次的mark做準備),如果該物件是不可達的,直接釋放記憶體。
sweep(start,end):
scan <- start
while scan < end
if isMarked(scan)
setUnMarked(scan)
else
free(scan)
scan <- nextObject(scan)
缺點
標記-清除演算法的比較大的缺點就是垃圾收集後有可能會造成大量的記憶體碎片,像上面的圖片所示,垃圾收集後記憶體中存在三個記憶體碎片,假設一個方格代表1個單位的記憶體,如果有一個物件需要佔用3個記憶體單位的話,那麼就會導致Mutator一直處於暫停狀態,而Collector一直在嘗試進行垃圾收集,直到Out of Memory。