JavaScript 垃圾回收入門

來源於 現代 JavaScript 教程
垃圾回收章節

雖然在 JavaScript 中不用本身管理內存,可是瞭解原理能夠在關鍵時候快速搜索到解決辦法,畢竟不少時候遇到問題是連搜索什麼關鍵詞都想不起來呀 😂javascript

在個人博客閱讀:https://ssshooter.com/2019-03...java

JavaScript 內存管理於咱們來講是自動的、不可見的。咱們建立的原始類型、對象、函數等等,都會佔用內存。git

當這些數據不被須要後會發生什麼?JavaScript 引擎如何發現並清除他們?github

可觸及(Reachability)

JavaScript 內存管理的關鍵概念是可觸及(Reachability)算法

簡單來講,「可觸及」的值就是可訪問的,可用的,他們被安全儲存在內存。編程

  1. 如下是一些一定「可觸及」的值,無論出於任何緣由,都不能刪除:安全

    • 當前函數的局部變量和參數。
    • 當前調用鏈(current chain of nested calls)中全部函數的局部變量和參數。
    • 全局變量。
    • (以及其餘內部變量)

這些值都稱爲 rootssh

  1. 其餘值是否可觸及視乎它是否被 root 及其引用鏈引用。

    假設有一個對象存在於局部變量,它的值引用了另外一個對象,若是這個對象是可觸及的,則它引用的對象也是可觸及的,後面會有詳細例子。函數

JavaScript 引擎有一個垃圾回收後臺進程,監控着全部對象,當對象不可觸及時會將其刪除。post

一個簡單例子

// user 引用了一個對象
let user = {
  name: 'John',
}

箭頭表明的是對象引用。全局變量 "user" 引用了對象 {name: "John"}(簡稱此對象爲 John)。John 的 "name" 屬性儲存的是一個原始值,因此無其餘引用。

若是覆蓋 user,對 John 的引用就丟失了:

user = null

如今 John 變得不可觸及,垃圾回收機制會將其刪除並釋放內存。

兩個引用

若是咱們從 user 複製引用到 admin

// user 引用了一個對象
let user = {
  name: 'John',
}

let admin = user

若是重複一次這個操做:

user = null

……這個對象是依然能夠經過 admin 訪問,因此它依然存在於內存。若是咱們把 admin 也覆蓋爲 null,那它就會被刪除了。

相互引用的對象

這個例子比較複雜:

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 如今已經不可觸及,將會被移除。

垃圾回收後:

孤島(Unreachable island)

也可能有一大堆互相引用的對象整塊(像個孤島)都不可觸及了。

對上面的對象進行操做:

family = null

內存中的狀況以下:

這個例子展現了「可觸及」這個概念的重要性。

儘管 John 和 Ann 互相依賴,但這仍不足夠。

"family" 對象整個已經切斷了與 root 的鏈接,沒有任何東西引用到這裏,因此這個孤島高不可攀,只能等待被清除。

內部算法

基礎的垃圾回收算法被稱爲「標記-清除算法」("mark-and-sweep"):

  • 垃圾回收器獲取並標記 root。
  • 而後訪問並標記來自他們的全部引用。
  • 訪問被標記的對象,標記他們的引用。全部被訪問過的對象都會被記錄,之後將不會重複訪問同一對象。
  • ……直到只剩下未訪問的引用。
  • 全部未被標記的對象都會被移除。

舉個例子,假設對象結構以下:

很明顯右側有一個「孤島」,如今使用「標記-清除」的方法處理它。

首先,標記 root:

而後標記他們的引用:

……標記他們引用的引用:

如今沒有被訪問過的對象會被認爲是不可觸及,他們將會被刪除:

這就是垃圾回收的工做原理。

JavaScript 引擎在不影響執行的狀況下作了不少優化,使這個過程的垃圾回收效率更高:

  • 分代收集 -- 對象會被分爲「新生代」和「老生代」。不少對象完成任務後很快就再也不須要了,因此對於他們的清理能夠很頻繁。而在清理中留下的稱爲「老生代」一員。
  • 增量收集 -- 若是對象不少,很難一次標記完全部對象,這個過程甚至對程序執行產生了明顯的延遲。因此引擎會嘗試把這個操做分割成多份,每次執行一份。這樣作要記錄額外的數據,可是能夠有效下降延遲對用戶體驗的影響。
  • 閒時收集 -- 垃圾回收器儘可能只在 CPU 空閒時運行,減小對程序執行的影響。

除此之外還有不少對垃圾回收的優化,在此就不詳細說了,各個引擎有本身的調整和技術,並且這個東西一直隨着引擎的更新換代在改變,若是不是有實在的需求,不值得挖太深。不過若是你真的對此有濃厚的興趣,下面會爲你提供一些拓展連接。

總結

重點:

  • 垃圾回收自動進行,咱們不能強制或阻止其進行。
  • 可觸及的對象會被保留在內存中。
  • 被引用不必定是可觸及的(從 root):相互引用的對象可能整塊都是不可觸及的。

《The Garbage Collection Handbook: The Art of Automatic Memory Management》(R. Jones 等)一書中說起了現代引擎實現的增強版垃圾回收算法。

若是你熟悉底層編程,能夠閱讀 A tour of V8: Garbage Collection 瞭解更多關於 V8 垃圾回收的細節。

V8 blog 也會常常發佈一些關於內存管理的文章。學習垃圾回收算法最好仍是先學習 V8 的實現,閱讀 Vyacheslav Egorov(V8 工程師之一)的博客。這裏說起 V8 是由於在互聯網上關於 V8 的文章比較多。對於其餘引擎,不少實現都是類似的,可是垃圾回收算法上區別不小。

對引擎的深刻理解在作底層優化的時候頗有幫助。在你熟悉一門語言以後,這是一個明智的研究方向。

相關文章
相關標籤/搜索