簡單瞭解JavaScript垃圾回收機制

NO IMAGE

注:本文主要針對初學GC的讀者,筆者對於GC的瞭解比較疏漏,有學習的慾望但終究時間太少,為了達到一個大致瞭解的程度,才寫筆記以理解之。文中有眾多用詞不當之處望讀者指正。

前言

學習並使用閉包的時候總會在各博客裡面看到閉包的壞處有一條:

使用不當的閉包將會在IE(IE9之前)中造成內存洩漏

uhhh,為什麼在IE9之前會造成這樣的結果呢?我就開始繼續尋找答案,找到一條比較滿意的:

IE9的JavaScript引擎使用的垃圾回收算法是引用計數法,對於循環引用將會導致GC無法回收“應該被回收”的內存。造成了無意義的內存佔用,也就是內存洩漏。

先科普一下:內存洩漏(Memory Leak)是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。

那麼,我有了以下幾個疑問:

  • 什麼是循環引用?
  • GC是什麼?它是怎麼工作的?
  • 為什麼引用計數算法將會導致內存無法釋放?
  • JavaScript(或者說JavaScript引擎)還有多少垃圾回收算法?

所以我就“科學上網”去不存在的Google搜了一下“JS的垃圾回收機制”,但是,前5篇出來的結果一眼就能看出是互相粘貼複製別人博客的,裡面無不提到這樣一段話:

在變量進入執行環境時,會添加一個進入標記,當變量離開時,會添加一個離開標記,標記清除是GC在運行時會給所有變量加上標記,然後去掉那些還在環境中或還被環境中變量引用的變量,清除剩下還被標記的所有變量。

抱歉,我理解能力有限,不明白“離開標記”是什麼,“然後去掉”是什麼時候去掉的,具體怎麼觸發的還是自動運行的。對此,我只能:

簡單瞭解JavaScript垃圾回收機制

所以,不得不自己看書了,在圖靈社區找到一本比較好的電子書《垃圾回收的算法與實現》,看了部分之後,基本對GC有了初步瞭解(起碼知道是怎麼工作的了)。

那麼下面,就逐步解決之前提到的幾點疑惑,並且用圖文結合的方式給大家提供一個快速理解的方法。當然,建議最好還是能夠去讀一讀上面提到的那邊電子書。

什麼是GC

GC是Garbage Collection的縮寫,意為垃圾回收,Uhh,說到垃圾回收,我想到的是下面這個場景:

簡單瞭解JavaScript垃圾回收機制

對!就是這個,在現實生活中我們會產生很多垃圾,總有一群人在我們還在睡覺或者外出工作的時候悄無聲息地把垃圾收走,那麼對於程序也是這樣的,在程序工作的過程中,總會產生很多’垃圾’,這些垃圾是程序不用的內存空間(可能是在之前用過了,以後不會再用的)。那麼GC就是負責收走垃圾的,因為他工作在JavaScript引擎內部,所以對於我們前端開發者來說,GC在“一定程度上”是悄無聲息工作的(注意此處的加引號部分)。

那麼,我們明確了GC做什麼了:

  • 找到內存空間中的垃圾。
  • 回收垃圾,讓程序員能再次利用這部分空間。

這裡要注意的是,不是所有語言的世界裡面都有GC,相對來說,高級語言裡面一般會帶GC,比如Java,JavaScriptPython,在沒有GC的世界裡,需要程序員手動管理內存,比如C語言我們常見的malloc/free,其實就是memory allocation的縮寫。當然,還有C++裡面的new/delete

還有一點,GC是一門古老但不過時的技術,在1960年就首次發佈了GC算法,但是時至今日,我們仍然需要研究更為優秀的GC算法來讓程序“更優秀”。

為什麼要使用GC

一句話:“省事兒”。

省去開發者手動管理內存的麻煩(不是每個開發者都能管理好),從而減少BUG的產生,把精力留給更本質的編程工作。

為什麼要學點GC

一句話:“為了更好地找BUG”。

作為前端開發者,其實是比較欠缺計算機體系知識的(以我自己舉例),比如操作系統,計算機組成原理和計算機網絡。但是這些知識,確實解決實際開發問題的根基所在,所以,有一個更好的基礎能帶來更快的開發/維護效率,而學習GC能為我們提供部分計算機體系知識的思想。比如”標記-清除法”就和操作系統頁面置換第二次機會算法類似,所以知識是融匯貫通的,這裡我們接觸了,那麼以後需要學習更多知識的時候就會更得心應手。

必備的基礎概念

  • 堆(HEAP)是用哦關於動態存放對象的內存空間對象在JavaScript裡面是引用類型,之前在我的另一篇博客有講JavaScript的類型

  • mutator,這個詞意思晦澀,在GC裡面代表應用程序本身,我們暫且理解為mutator需要大量的內存。

  • allocatormutator將需要內存的申請提交到此,allocator負責從堆中調取足夠內存空間供mutator使用。

一張圖理解之:

簡單瞭解JavaScript垃圾回收機制

  • 活動對象/非活動對象:代表通過mutator引用的對象,舉個例子:
var a = {name: 'bar'} // '這個對象'被a引用,是活動對象。
a=null; // ‘這個對象’沒有被a引用了,這個對象是非活動對象。

常用的幾種GC算法

引用計數法

簡單瞭解JavaScript垃圾回收機制

(圖源自《垃圾回收算法和實現》)

顧名思義,讓所有對象實現記錄下有多少“程序”在引用自己,讓各對象都知道自己的“人氣指數”。舉一個簡單的例子:

var a = new Object(); // 此時'這個對象'的引用計數為1(a在引用)
var b = a; // ‘這個對象’的引用計數是2(a,b)
a = null; // reference_count = 1
b = null; // reference_count = 0 
// 下一步 GC來回收‘這個對象’了

這個方法有優勢也有劣勢:

優勢

  1. 可即刻回收垃圾,當被引用數值為0時,對象馬上會把自己作為空閒空間連到空閒鏈表上,也就是說。在變成垃圾的時候就立刻被回收。
  2. 因為是即時回收,那麼‘程序’不會暫停去單獨使用很長一段時間的GC,那麼最大暫停時間很短。
  3. 不用去遍歷堆裡面的所有活動對象和非活動對象

劣勢

  1. 計數器需要佔很大的位置,因為不能預估被引用的上限,打個比方,可能出現32位即2的32次方個對象同時引用一個對象,那麼計數器就需要32位。
  2. 最大的劣勢是無法解決循環引用無法回收的問題 這就是前文中IE9之前出現的問題

一個簡單的例子:

function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2,o2的引用次數是1
o2.a = o; // o2 引用 o,o的引用此時是1
return "azerty";
}
f();

fn在執行完成之後理應回收fn作用域裡面的內存空間,但是因為o裡面有一個屬性引用o2,導致o2的引用次數始終為1,o2也是如此,而又非專門當做閉包來使用,所以這裡就應該使oo2被銷燬。

因為算法是將引用次數為0的對象銷燬,此處都不為0,導致GC不會回收他們,那麼這就是內存洩漏問題。

該算法已經逐漸被 ‘標記-清除’ 算法替代,在V8引擎裡面,使用最多的就是 標記-清除算法

標記清除算法

簡單瞭解JavaScript垃圾回收機制

主要將GC的垃圾回收過程分為兩個階段

  • 標記階段:把所有活動對象做上標記。
  • 清除階段:把沒有標記(也就是非活動對象)銷燬。

標記階段

簡單瞭解JavaScript垃圾回收機制

可以理解成我們的全局作用域,GC從全局作用域的變量,沿作用域逐層往裡遍歷(對,是深度遍歷),當遍歷到堆中對象時,說明該對象被引用著,則打上一個標記,繼續遞歸遍歷(因為肯定存在堆中對象引用另一個堆中對象),直到遍歷到最後一個(最深的一層作用域)節點。

簡單瞭解JavaScript垃圾回收機制

標記完成之後,就是這樣的:

簡單瞭解JavaScript垃圾回收機制

清除階段

又要遍歷,這次是遍歷整個堆,回收沒有打上標記的對象

這裡我們不細講如何將獲得的內存空間再分配的問題,這個地方有點類似磁盤管理或者內存管理,比如best-fit,First-fit,Worst-fit。以及碎片化問題的產生和解決方法。

這種方法可以解決循環引用問題,因為兩個對象從全局對象出發無法獲取。因此,他們無法被標記,他們將會被垃圾回收器回收。正如圖:

簡單瞭解JavaScript垃圾回收機制

優勢:

  • 實現簡單,打標記也就是打或者不打兩種可能,所以就一位二進制位就可以表示
  • 解決了循環引用問題

缺點

  • 造成碎片化(有點類似磁盤的碎片化)
  • 再分配時遍次數多,如果一直沒有找到合適的內存塊大小,那麼會遍歷空閒鏈表(保存堆中所有空閒地址空間的地址形成的鏈表)一直遍歷到尾端

這種GC方式是一個定時運行的任務,也就是說當程序運行一段時間後,統一GC,類似如圖:

簡單瞭解JavaScript垃圾回收機制

複製算法

簡單瞭解JavaScript垃圾回收機制

複製算法配合這張圖理解起來非常簡單,就是隻把某個空間的活動對象複製到其他空間。

將一個內存空間分為兩部分,一部分是From空間,另一部分是To空間,將From空間裡面的活動對象複製到To空間,然後釋放掉整個From空間,然後此刻將From空間和To空間的身份互換,那麼就完成了一次GC。

如圖所示:

簡單瞭解JavaScript垃圾回收機制

還有

還有幾個GC算法,準備在下一篇博客用圖解的方式總結一下V8實現的GC算法(複製,標記-清除,壓縮)。

總結

回顧我們之前的問題

  • 什麼是循環引用?
  • GC是什麼?它是怎麼工作的?
  • 為什麼引用計數算法將會導致內存無法釋放?
  • JavaScript(或者說JavaScript引擎)還有多少垃圾回收算法?

好好想想我們是不是都應該有個比較清楚的答案了呢?

相關文章

如何用MacBook提高工作效率【配置篇】

如何用MacBook提高工作效率的【工具篇】

電商網站項目總結:Vuex帶來全新的編程體驗

輕鬆掌握移動端web開發【尺寸適配】常用解決方案