簡單理解 JavaScript 的記憶體管理機制

不像其他的語言,JavaScript 開發者永遠沒有辦法自己去釋放記憶體,頂多只能移除物件的 Reference (代表這物件已經沒有人在使用),而且這物件所佔的記憶體並不會馬上被釋放,而是 Garbage Collection 在滿足某些條件的情況下,才在背景自動去尋找沒有被使用的物件,然後釋放。若你嘗試過尋找釋放記憶體或移除物件 Reference 的方法,得到的解答,應該不外乎是使用 delete 關鍵字或是將變數設為 null,但是你真的了解它的意義嗎?事實上,有很多人都是在不瞭解的情況下使用它們,還可能因此產生 Memory leaks 的狀況。欲弄清楚這一切,我們必須先簡單理解一下 JavaScript 的記憶體管理機制,更準確的說,是物件的管理機制。

從 JavaScript 開發者角度來看,JavaScript Engine 在運作時,記憶體使用是呈現樹狀結構,也就是所有命名或建立的變數或物件,都是存放在一個全域(global)的 Object 中。在 Node.js 中,你可以直接讀取 global 變數看到目前 Context 的環境狀態:
console.log(global);

我們可以做個實驗理解一下:
var myVar = 'Hello';
function myFunc() {
    return 123;
}
var myObj = {
    a: 1,
    b: 2
};

console.log(global);

執行以上程式,你應該可以從 global 中找到我們自己定義的變數和函式:
{
    ...(已省略基本預設的環境變數)...
    myVar: 'Hello',
    myFunc: [function],
    myObj: {
        a: 1,
        b: 2
    },
    ...
}

從結果可以發現,所有的物件都以樹狀的形式被 global Object 保存著,無論是變數還是任何一種類型的物件,都是一組組 Key/Value 的存在。而 Value 就是各種不同形態的物件,如字串、函數、陣列、數值等。

所以,移除某物件的 Reference,就意味著將把物件從這棵樹上拔除掉。因此,我們可以直接將該變數設為 null:
myVar = null;

由於該變數被設為 null,原本的字串(包含著『Hello』)物件就失去了依附的樹枝,如枯葉般從樹上掉下來,等著 Garbage Collection 來回收它。對於開發者而言,其實就是告訴 GC 我不需要這物件了,隨時可以把這個物件的記憶體釋放。

然而,雖然變數被設為 null 後,原本的物件被釋放了,但該變數還是存在的,別忘了,他是一個在 global Object 中的 Key,現在只是沒有 Value 為 null 而已。要真正把這個變數給刪除,這時就要用到 delete 關鍵字。如果你去查一下 JavaScript 的 API 參考文獻,就會發現 delete 關鍵字其實是拿來刪除 Object 中的一組 Key/Value。因此,既然 JavaScript 所有的變數其實都只是一組存放在 global Object 的 Key/Value,我們理所當然可以用 delete 關鍵字去移除掉他:
delete myVar;

知曉了 JavaScript 的記憶體管理機制後,你就會了解使用 delete 關鍵字和將變數設為 null,其實並不是代表物件就會被釋放,只是砍樹枝去減少物件的 Reference。

此外,如果一個物件有多個 Reference,只是單單刪其中一個也不會讓物件被 GC 釋放:
var myVar = 'Hello';
var myVar1 = myVar;

myVar = null;
delete myVar;

console.log(myVar1);

以上的程式會顯示『Hello』字串,該物件並不會因為失去 myVar 這 Reference 而被 GC 移除。若想要這一個字串被釋放,必需清空物件所有的 Reference(包括 myVar 和 myVar1),才能讓物件具有被 GC 回收的條件。所以,如果你不小心讓一個不明顯的變數勾搭上了物件,然後你忘記了這個變數的存在,很有可能就會造成 Memory Leaks,讓以為已經被釋放的物件,偷偷存活在於記憶體上。

備註:Reference 是常見於各種系統的設計,主要做法是幫物件建立一個 Reference 計數器,當有人關聯或使用到他,就會讓這計數器加一,等到關聯被移除或使用完畢後,就會讓計數器減一。所以,一旦計數器為零時,代表現在沒有任何外部的物件在使用或關聯到它,是可以被釋放掉的狀態。

後記

這是很多人的誤解,在 JavaScript 中,千萬不要傻傻的以為用 delete 關鍵字就可以把物件給釋放了,需要特別注意。:-)

留言

  1. 這 keyword 應該改成 unset 比較好 XD

    var i = 'hello!';
    var j = i;

    unset i; // 從 global 裡面移掉 i 這變數(但是 j 還是參考到該物件。)

    回覆刪除
    回覆
    1. 剛試過,unset 的確有作用。

      但是,unset 似乎不是目前 ECMAScript 的標準,所以就不建議使用了。 :-P

      http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf

      刪除
  2. 正好解決了正在處理的JS記憶體問題

    感謝

    回覆刪除

張貼留言

這個網誌中的熱門文章

有趣的邏輯問題:是誰在說謊

Web 技術中的 Session 是什麼?

淺談 USB 通訊架構之定義(二)

淺談 USB 通訊架構之定義(一)

Reverse SSH Tunnel 反向打洞實錄