垃圾回收的標記清除演算法詳解

antzone發表於2018-06-06

JavaScript有內建的垃圾回收機制,可以使我們免去考慮記憶體分配和釋放的操作問題,從而可以花費更多的精力去完成功能的實現。但是對於它的瞭解還是必不可少的,以便寫出更為優化的程式碼。

當前最基本的垃圾回收演算法有四種:

(1).標記-清除演算法(mark-sweep)。

(2).標記-壓縮演算法(mark-compact)。

(3).複製演算法(copying)。

(4).引用計數演算法(reference counting)。

當前瀏覽器的垃圾回收機制通常是由上述幾種演算法混合而成,本章節就單獨介紹比較流行的標記清除演算法。

一.基本概念:

在垃圾回收的演算法中,經常會出現mutator和collector兩個名稱,下面就對它做一下簡單介紹。

(1).collector:指的就是垃圾收集器。

(2).mutator:指的是垃圾收集器之外的部分,比如當前的應用程式。它的功能是建立新物件,或者在記憶體讀寫內容。

(3).mutator roots:mutator根物件,通常是分配在堆記憶體之外,可以直接被mutator直接訪問到的物件,一般是指靜態/全域性變數。

(4).可到達物件:所謂的可到達物件就是從根物件開始遍歷,可以訪問到的物件,也就是mutator(應用程式)正在使用的物件。

二.演算法原理:

標記清除演算法從名稱上看,可以拆分為兩部分:標記(mark)和清除(sweep)。

此演算法可以分為兩個階段,一個是標記階段,一個是清除階段,下面就分別做一下介紹。

(1).標記階段:

在此階段,垃圾回收器會從mutator(應用程式)根物件開始遍歷。

每一個可以從根物件訪問到的物件都會被新增一個標識,於是這個物件就被標識為可到達物件。

(2).清除階段:

在此階段中,垃圾回收器,會對堆記憶體從頭到尾進行線性遍歷,如果發現有物件沒有被標識為可到達物件,那麼就將此物件佔用的記憶體回收,並且將原來標記為可到達物件的標識清除,以便進行下一次垃圾回收操作。

圖示如下:

a:3:{s:3:\"pic\";s:43:\"portal/201704/10/130136v16uyge0d6naeodd.png\";s:5:\"thumb\";s:0:\"\";s:6:\"remote\";N;}

在標記階段,從跟對想1可以訪問到B,從B又可以訪問到E,那麼B和E都是可到達物件,同樣的道理,F、G、J和K都是可到達物件。在回收階段,所有未標記為可到達的物件都會被垃圾回收器回收。

特別說明:在垃圾回收階段,應用程式的執行會暫停,等待回收執行完畢後,再恢復程式的執行。

三.何時開始垃圾回收:

在使用標記清除演算法時,未引用物件並不會被立即回收.取而代之的做法是,垃圾物件將一直累計到記憶體耗盡為止.當記憶體耗盡時,程式將會被掛起,垃圾回收開始執行。

四.垃圾回收虛擬碼表示:

[JavaScript] 純文字檢視 複製程式碼
New():
  //分配新的記憶體到ref指標
  ref <- allocate()  
  if ref == null
    //記憶體不足,則觸發垃圾收集
    collect()  
  ref <- allocate()
  if ref == null
    //垃圾收集後仍然記憶體不足,則丟擲Out of Memory錯誤
    throw "Out of Memory"   
  return ref
 
  atomic collect():
  markFromRoots()
  sweep(HeapStart,HeapEnd)

上面的程式碼表示了垃圾回收是如何被觸發的。

[JavaScript] 純文字檢視 複製程式碼
markFromRoots():
  worklist <- empty
  //遍歷所有mutator根物件
  for each fld in Roots
    ref <- *fld
      //如果它是可達的而且沒有被標記的,直接標記該物件並將其加到worklist中
      if ref != null && isNotMarked(ref)
        setMarked(ref)
        add(worklist,ref)
        mark()
mark():
  while not isEmpty(worklist)
    //將worklist的最後一個元素彈出,賦值給ref
    ref <- remove(worklist)
    //遍歷ref物件的所有指標域,如果其指標域(child)是可達的,直接標記其為可達物件並且將其加入worklist中
    for each fld in Pointers(ref)
      //通過這樣的方式來實現深度遍歷,直到將該物件下面所有可以訪問到的物件都標記為可達物件。
      child <- *fld
        if child != null && isNotMarked(child)
          setMarked(child)
          add(worklist,child)

上面的是mark標記的演算法。

[JavaScript] 純文字檢視 複製程式碼
sweep(start,end):
    scan <- start
   while scan < end
       if isMarked(scan)
          setUnMarked(scan)
      else
          free(scan)
      scan <- nextObject(scan)

上面的程式碼表示的是垃圾回收的sweep階段。

五.標記清除演算法的缺點:

垃圾收集後有可能會造成大量的記憶體碎片,像上面的圖片所示,垃圾收集後記憶體中存在三個記憶體碎片,假設一個方格代表1個單位的記憶體,如果有一個物件需要佔用3個記憶體單位的話,那麼就會導致Mutator一直處於暫停狀態,而Collector一直在嘗試進行垃圾收集,直到Out of Memory。

相關文章