垃圾回收機制

垃圾回收(garbage collection)簡稱為GC

為何要垃圾回收?

JavaScript程式每次創建字元串、數組或對象時,解釋器都必須分配記憶體來存儲那個實體。只要像這樣動態地分配了記憶體,最終都要釋放這些記憶體以便他們能夠被再用,否則,JavaScript的解釋器將會消耗完系統中所有可用的記憶體,造成系統崩潰。

不同語言的垃圾回收策略

手動回收

何時分配記憶體、何時銷毀記憶體都是由程式碼控制的,如 C/C++。

自動回收

垃圾數據是由垃圾回收器來釋放的,並不需要手動通過程式碼來釋放,如 JavaScript、Java、Python 等語言

垃圾回收的方式

標記清除(mark and sweep)

  大部分瀏覽器以此方式進行垃圾回收,當變數進入執行環境(函數中聲明變數,執行時)的時候,垃圾回收器將其標記為「進入環境」,當變數離開環境的時候(函數執行結束)將其標記為「離開環境」,在離開環境之後還有的變數則是需要被刪除的變數。標記方式不定,可以是某個特殊位的反轉或維護一個列表等。

  垃圾收集器給記憶體中的所有變數都加上標記,然後去掉環境中的變數以及被環境中的變數引用的變數的標記。在此之後再被加上的標記的變數即為需要回收的變數,因為環境中的變數已經無法訪問到這些變數。

引用計數

  另一種不太常見的垃圾回收策略是引用計數。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變數並將一個引用類型賦值給該變數時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其所佔的記憶體空間給收回來。這樣,垃圾收集器下次再運行時,它就會釋放那些引用次數為0的值所佔的記憶體。 該方式會引起記憶體泄漏的原因是它不能解決循環引用的問題。

低版本IE中有一部分對象並不是原生JS對象。例如,其BOM和DOM中的對象就是使用C++以COM(Component Object Model)對象的形式實現的,而COM對象的垃圾收集機制採用的就是引用計數策略。

因此即使IE的js引擎是用的標記清除來實現的,但是js訪問COM對象如BOM,DOM還是基於引用計數的策略的,也就是說只要在IE中設計到COM對象,也就會存在循環引用的問題。

 

JS里的垃圾回收機制

首先我們要明確JS里的數據存儲分為棧存儲和堆存儲,這兩者採用的垃圾回收機制是不同的。

棧的垃圾回收機制

通過移動ESP指針(記錄當前執行狀態的指針)實現垃圾回收。執行棧中當一個函數執行完畢,JavaScript 引擎會通過向下移動 ESP 來銷毀該函數保存在棧中的執行上下文。

例:

function foo(){ 
  var a = 1;
  var b = {name:"極客邦"}
  function showName() { 
    var c = "極客時間";
    var d = {name:"極客時間"};
  } 
  showName()
    }
foo()

當程式碼執行到第六行時堆棧狀態

image.png

當 showName 函數執行完成之後,函數執行流程就進入了 foo 函數,那這時就需要銷毀 showName 函數的執行上下文了。ESP 這時候就幫上忙了,JavaScript 會將 ESP 下移到 foo 函數的執行上下文,這個下移操作就是銷毀 showName 函數執行上下文的過程。

移動 ESP 前後的對比圖:

image.png

從圖中可以看出,當 showName 函數執行結束之後,ESP 向下移動到 foo 函數的執行上下文中,上面 showName 的執行上下文雖然保存在棧記憶體中,但是已經是無效記憶體了。比如當 foo 函數再次調用另外一個函數時,這塊內容會被直接覆蓋掉,用來存放另外一個函數的執行上下文。

堆的垃圾回收機制

代際假說(The Generational Hypothesis)

後續垃圾回收的策略都是建立在該假說的基礎之上的。

特徵:

  • 大部分對象在記憶體中存在的時間很短,簡單來說,就是很多對象一經分配記憶體,很快就變得不可訪問
  • 不死的對象,會活得更久

新老生代區別

在V8 中會把堆分為新生代老生代兩個區域,兩者的區別主要在一下幾點

 

新生代

老生代

存儲對象

生存時間短的對象

生存時間久的對象

記憶體大小

32位下16MB

64位下64MB

 

32位下700MB

64位下1400MB

 

所用垃圾回收器

副垃圾回收器

垃圾回收器

垃圾回收器共有的的工作流程

  1. 標記空間中活動對象和非活動對象。所謂活動對象就是還在使用的對象,非活動對象就是可以進行垃圾回收的對象。
  2. 回收非活動對象所佔據的記憶體。其實就是在所有的標記完成之後,統一清理記憶體中所有被標記為可回收的對象。
  3. 記憶體整理。一般來說,頻繁回收對象後,記憶體中就會存在大量不連續空間,我們把這些不連續的記憶體空間稱為記憶體碎片。當記憶體中出現了大量的記憶體碎片之後,如果需要分配較大連續記憶體的時候,就有可能出現記憶體不足的情況。所以最後一步需要整理這些記憶體碎片,但這步其實是可選的,因為有的垃圾回收器不會產生記憶體碎片,比如副垃圾回收器。

 

副垃圾回收器

職責:負責新生區的垃圾回收。

回收方式:Scavenge 演算法。即把新生代空間對半劃分為兩個區域,一半是對象區域,一半是空閑區域

image.png

具體步驟:

  1. 對對象區域中的垃圾做標記;
  2. 標記完成之後,就進入垃圾清理階段,副垃圾回收器會把這些存活的對象複製到空閑區域中,同時它還會把這些對象有序地排列起來,所以這個複製過程,也就相當於完成了記憶體整理操作,複製後空閑區域就沒有記憶體碎片了。
  3. 完成複製後,對象區域與空閑區域進行角色翻轉,也就是原來的對象區域變成空閑區域,原來的空閑區域變成了對象區域。這樣就完成了垃圾對象的回收操作,同時這種角色翻轉的操作還能讓新生代中的這兩塊區域無限重複使用下去。

為什麼新生代空間比較小?怎麼優化空間小的問題

由於新生代中採用的 Scavenge 演算法,所以每次執行清理操作時,都需要將存活的對象從對象區域複製到空閑區域。但複製操作需要時間成本,如果新生區空間設置得太大了,那麼每次清理的時間就會過久,所以為了執行效率,一般新生區的空間會被設置得比較小。

也正是因為新生區的空間不大,所以很容易被存活的對象裝滿整個區域。為了解決這個問題,JavaScript 引擎採用了對象晉陞策略,也就是經過兩次垃圾回收依然還存活的對象,會被移動到老生區中。

 

主垃圾回收器

職責:負責老生區的垃圾回收。

回收方式:標記 – 清除(Mark-Sweep),標記 – 整理(Mark-Compact)

對象特徵:

  1. 是對象佔用空間大
  2. 對象存活時間長

注意⚠️:老生區除了新生區中晉陞的對象,一些大的對象會直接被分配到老生區

具體步驟:

  1. 標記階段,從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數據
  2. 清除垃圾過程。它和副垃圾回收器的垃圾清除過程完全不同,你可以理解這個過程是清除掉紅色標記數據的過程,可參考下圖大致理解下其清除過程:

image.png

3.標記 – 整理,由於對一塊記憶體多次執行標記 – 清除演算法後,會產生大量不連續的記憶體碎片。而碎片過多會導致大對象無法分配到足夠的連續記憶體,所以會執行標記整理,讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。

 

百科百科

極客時間:瀏覽器工作原理與實踐