JVM--標記-清除算法Mark-Sweep


前言

垃圾自動回收機制的出現使編程更加的簡單,使得我們不需要再去考慮內存分配和釋放的問題,而是更加的專注在我們產品功能的實現上。但是我們還是需要花時間去了解下垃圾收集機制是怎么工作的,以便后面能夠更好的進行我們應用的性能調優等。

目前最基本的垃圾收集算法有四種,標記-清除算法(mark-sweep),標記-壓縮算法(mark-compact),復制算法(copying)以及引用計數算法(reference counting).而現代流行的垃圾收集算法一般是由這四種中的其中幾種算法相互組合而成,比如說,對堆(heap)的一部分采用標記-清除算法,對堆(heap)的另外一部分則采用復制算法等等。今天我們主要來看下標記-清除算法的原理。

基本概念

在了解標記-清除算法前,我們先要了解幾個基本概念。

首先是mutator和collector,這兩個名詞經常在垃圾收集算法中出現,collector指的就是垃圾收集器,而mutator是指除了垃圾收集器之外的部分,比如說我們應用程序本身。mutator的職責一般是NEW(分配內存),READ(從內存中讀取內容),WRITE(將內容寫入內存),而collector則就是回收不再使用的內存來供mutator進行NEW操作的使用。

第二個基本概念是關於mutator roots(mutator根對象),mutator根對象一般指的是分配在堆內存之外,可以直接被mutator直接訪問到的對象,一般是指靜態/全局變量以及Thread-Local變量(在Java中,存儲在java.lang.ThreadLocal中的變量和分配在棧上的變量 - 方法內部的臨時變量等都屬於此類). 

第三個基本概念是關於可達對象的定義,從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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM