圖解 JavaScript 垃圾回收 — 現代 JavaScript 教程

垃圾回收

對於開發者來講,JavaScript 的內存管理是自動的、無形的。咱們建立的原始值、對象、函數……這一切都會佔用內存。javascript

當咱們再也不須要某個東西時會發生什麼?JavaScript 引擎如何發現它並清理它?html

可達性(Reachability)

JavaScript 中主要的內存管理概念是 可達性java

簡而言之,「可達」值是那些以某種方式可訪問或可用的值。它們必定是存儲在內存中的。react

  1. 這裏列出固有的可達值的基本集合,這些值明顯不能被釋放。算法

    比方說:編程

    • 當前函數的局部變量和參數。
    • 嵌套調用時,當前調用鏈上全部函數的變量與參數。
    • 全局變量。
    • (還有一些內部的)

    這些值被稱做 根(roots)微信

  2. 若是一個值能夠經過引用或引用鏈從根訪問任何其餘值,則認爲該值是可達的。編程語言

    比方說,若是局部變量中有一個對象,而且該對象有一個屬性引用了另外一個對象,則該對象被認爲是可達的。並且它引用的內容也是可達的。下面是詳細的例子。函數

在 JavaScript 引擎中有一個被稱做 垃圾回收器 的東西在後臺執行。它監控着全部對象的狀態,並刪除掉那些已經不可達的。post

一個簡單的例子

這裏是一個最簡單的例子:

// user 具備對這個對象的引用
let user = {
  name: "John"
};
複製代碼

這裏的箭頭描述了一個對象引用。全局變量 "user" 引用了對象 {name:"John"}(爲簡潔起見,咱們稱它爲 John)。John 的 "name" 屬性存儲一個原始值,因此它被寫在對象內部。

若是 user 的值被重寫了,這個引用就沒了:

user = null;
複製代碼

如今 John 變成不可達的了。由於沒有引用了,就不能訪問到它了。垃圾回收器會認爲它是垃圾數據並進行回收,而後釋放內存。

兩個引用

如今讓咱們想象下,咱們把 user 的引用複製給 admin

// user 具備對這個對象的引用
let user = {
  name: "John"
};

let admin = user;
複製代碼

如今若是執行剛剛的那個操做:

user = null;
複製代碼

……而後對象仍然能夠被經過 admin 這個全局變量訪問到,因此對象還在內存中。若是咱們又重寫了 admin,對象就會被刪除。

相互關聯的對象

如今來看一個更復雜的例子。這是個家庭:

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});
複製代碼

marry 函數經過讓兩個對象相互引用使它們「結婚」了,並返回了一個包含這兩個對象的新對象。

由此產生的內存結構:

到目前爲止,全部對象都是可達的。

如今讓咱們移除兩個引用:

delete family.father;
delete family.mother.husband;
複製代碼

僅刪除這兩個引用中的一個是不夠的,由於全部的對象仍然都是可達的。

可是,若是咱們把這兩個都刪除,那麼咱們能夠看到再也沒有對 John 的引用了:

對外引用不重要,只有傳入引用纔可使對象可達。因此,John 如今是不可達的,而且將被從內存中刪除,同時 John 的全部數據也將變得不可達。

通過垃圾回收:

沒法到達的島嶼

幾個對象相互引用,但外部沒有對其任意對象的引用,這些對象也多是不可達的,並被從內存中刪除。

源對象與上面相同。而後:

family = null;
複製代碼

內存內部狀態將變成:

這個例子展現了可達性概念的重要性。

顯而易見,John 和 Ann 仍然連着,都有傳入的引用。可是,這樣還不夠。

前面說的 "family" 對象已經再也不與根相連,沒有了外部對其的引用,因此它變成了一座「孤島」,而且將被從內存中刪除。

內部算法

垃圾回收的基本算法被稱爲 "mark-and-sweep"。

按期執行如下「垃圾回收」步驟:

  • 垃圾收集器找到全部的根,並「標記」(記住)它們。
  • 而後它遍歷並「標記」來自它們的全部引用。
  • 而後它遍歷標記的對象並標記 他們的 引用。全部被遍歷到的對象都會被記住,以避免未來再次遍歷到同一個對象。
  • ……如此操做,直到全部可達的(從根部)引用都被訪問到。
  • 沒有被標記的對象都會被刪除。

例如,使咱們的對象有以下的結構:

咱們能夠清楚地看到右側有一個「沒法到達的島嶼」。如今咱們來看看「標記和清除」垃圾收集器如何處理它。

第一步標記全部的根:

而後他們的引用被標記了:

……若是還有引用的話,繼續標記:

如今,沒法經過這個過程訪問到的對象被認爲是不可達的,而且會被刪除。

咱們還能夠將這個過程想象成從根溢出一個巨大的油漆桶,它流經全部引用並標記全部可到達的對象。而後移除未標記的。

這是垃圾收集工做的概念。JavaScript 引擎作了許多優化,使垃圾回收運行速度更快,而且不影響正常代碼運行。

一些優化建議:

  • 分代收集 —— 對象被分紅兩組:「新的」和「舊的」。許多對象出現,完成他們的工做並很快死去,他們能夠很快被清理。那些長期存活的對象會變得「老舊」,並且被檢查的頻次也會減小。
  • 增量收集 —— 若是有許多對象,而且咱們試圖一次遍歷並標記整個對象集,則可能須要一些時間,並在執行過程當中帶來明顯的延遲。因此引擎試圖將垃圾收集工做分紅幾部分來作。而後將這幾部分會逐一進行處理。這須要他們之間有額外的標記來追蹤變化,可是這樣會有許多微小的延遲而不是一個大的延遲。
  • 閒時收集 —— 垃圾收集器只會在 CPU 空閒時嘗試運行,以減小可能對代碼執行的影響。

還有其餘垃圾回收算法的優化和風格。儘管我想在這裏描述它們,但我必須打住了,由於不一樣的引擎會有不一樣的調整和技巧。並且,更重要的是,隨着引擎的發展,狀況會發生變化,因此在沒有真實需求的時候,「提早」學習這些內容是不值得的。固然,除非這是一個純粹的利益關係。我在下面給你提供了一些相關連接。

總結

主要須要掌握的內容:

  • 垃圾回收是自動完成的,咱們不能強制執行或是阻止執行。
  • 當對象是可達狀態時,它必定是存在於內存中的。
  • 被引用與可訪問(從一個根)不一樣:一組相互鏈接的對象可能總體都不可達。

現代引擎實現了垃圾回收的高級算法。

《The Garbage Collection Handbook: The Art of Automatic Memory Management》(R. Jones 等人著)這本書涵蓋了其中一些內容。

若是你熟悉底層(low-level)編程,關於 V8 引擎垃圾回收器的更詳細信息請參閱文章 V8 之旅:垃圾回收

V8 博客 還不時發佈關於內存管理變化的文章。固然,爲了學習垃圾收集,你最好經過學習 V8 引擎內部知識來進行準備,並閱讀一個名爲 Vyacheslav Egorov 的 V8 引擎工程師的博客。我之因此說 「V8」,由於網上關於它的文章最豐富的。對於其餘引擎,許多方法是類似的,但在垃圾收集上許多方面有所不一樣。

當你須要底層的優化時,對引擎有深刻了解將頗有幫助。在熟悉了這門編程語言以後,把熟悉引擎做爲下一步計劃是明智之選。

本文首發於微信公衆號「技術漫談」,歡迎微信搜索關注,訂閱更多精彩內容。


現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。React 官方文檔推薦,與 MDN 並列的 JavaScript 學習教程

在線免費閱讀:zh.javascript.info


掃描下方二維碼,關注微信公衆號「技術漫談」,訂閱更多精彩內容。

相關文章
相關標籤/搜索