Javascript 垃圾回收

2023-02-17 10:38 更新

對(duì)于開(kāi)發(fā)者來(lái)說(shuō),JavaScript 的內(nèi)存管理是自動(dòng)的、無(wú)形的。我們創(chuàng)建的原始值、對(duì)象、函數(shù)……這一切都會(huì)占用內(nèi)存。

當(dāng)我們不再需要某個(gè)東西時(shí)會(huì)發(fā)生什么?JavaScript 引擎如何發(fā)現(xiàn)它并清理它?

可達(dá)性(Reachability)

JavaScript 中主要的內(nèi)存管理概念是 可達(dá)性。

簡(jiǎn)而言之,“可達(dá)”值是那些以某種方式可訪問(wèn)或可用的值。它們一定是存儲(chǔ)在內(nèi)存中的。

  1. 這里列出固有的可達(dá)值的基本集合,這些值明顯不能被釋放。
  2. 比方說(shuō):

    • 當(dāng)前執(zhí)行的函數(shù),它的局部變量和參數(shù)。
    • 當(dāng)前嵌套調(diào)用鏈上的其他函數(shù)、它們的局部變量和參數(shù)。
    • 全局變量。
    • (還有一些內(nèi)部的)

    這些值被稱作 根(roots)。

  3. 如果一個(gè)值可以通過(guò)引用鏈從根訪問(wèn)任何其他值,則認(rèn)為該值是可達(dá)的。
  4. 比方說(shuō),如果全局變量中有一個(gè)對(duì)象,并且該對(duì)象有一個(gè)屬性引用了另一個(gè)對(duì)象,則  對(duì)象被認(rèn)為是可達(dá)的。而且它引用的內(nèi)容也是可達(dá)的。下面是詳細(xì)的例子。

在 JavaScript 引擎中有一個(gè)被稱作 垃圾回收器 的東西在后臺(tái)執(zhí)行。它監(jiān)控著所有對(duì)象的狀態(tài),并刪除掉那些已經(jīng)不可達(dá)的。

一個(gè)簡(jiǎn)單的例子

這里是一個(gè)最簡(jiǎn)單的例子:

// user 具有對(duì)這個(gè)對(duì)象的引用
let user = {
  name: "John"
};


這里的箭頭描述了一個(gè)對(duì)象引用。全局變量 "user" 引用了對(duì)象 {name:"John"}(為簡(jiǎn)潔起見(jiàn),我們稱它為 John)。John 的 "name" 屬性存儲(chǔ)一個(gè)原始值,所以它被寫(xiě)在對(duì)象內(nèi)部。

如果 user 的值被重寫(xiě)了,這個(gè)引用就沒(méi)了:

user = null;


現(xiàn)在 John 變成不可達(dá)的了。因?yàn)闆](méi)有引用了,就不能訪問(wèn)到它了。垃圾回收器會(huì)認(rèn)為它是垃圾數(shù)據(jù)并進(jìn)行回收,然后釋放內(nèi)存。

兩個(gè)引用

現(xiàn)在讓我們想象下,我們把 user 的引用復(fù)制給 admin

// user 具有對(duì)這個(gè)對(duì)象的引用
let user = {
  name: "John"
};

let admin = user;


現(xiàn)在如果執(zhí)行剛剛的那個(gè)操作:

user = null;

……然后對(duì)象仍然可以被通過(guò) admin 這個(gè)全局變量訪問(wèn)到,因此它必須被保留在內(nèi)存中。如果我們又重寫(xiě)了 admin,對(duì)象就會(huì)被刪除。

相互關(guān)聯(lián)的對(duì)象

現(xiàn)在來(lái)看一個(gè)更復(fù)雜的例子。這是個(gè)家庭:

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

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

marry 函數(shù)通過(guò)讓兩個(gè)對(duì)象相互引用使它們“結(jié)婚”了,并返回了一個(gè)包含這兩個(gè)對(duì)象的新對(duì)象。

由此產(chǎn)生的內(nèi)存結(jié)構(gòu):


到目前為止,所有對(duì)象都是可達(dá)的。

現(xiàn)在讓我們移除兩個(gè)引用:

delete family.father;
delete family.mother.husband;


僅刪除這兩個(gè)引用中的一個(gè)是不夠的,因?yàn)樗械膶?duì)象仍然都是可達(dá)的。

但是,如果我們把這兩個(gè)都刪除,那么我們可以看到再也沒(méi)有對(duì) John 的引用了:


對(duì)外引用不重要,只有傳入引用才可以使對(duì)象可達(dá)。所以,John 現(xiàn)在是不可達(dá)的,并且將被從內(nèi)存中刪除,同時(shí) John 的所有數(shù)據(jù)也將變得不可達(dá)。

經(jīng)過(guò)垃圾回收:


無(wú)法到達(dá)的島嶼

幾個(gè)對(duì)象相互引用,但外部沒(méi)有對(duì)其任意對(duì)象的引用,這些對(duì)象也可能是不可達(dá)的,并被從內(nèi)存中刪除。

源對(duì)象與上面相同。然后:

family = null;

內(nèi)存內(nèi)部狀態(tài)將變成:


這個(gè)例子展示了可達(dá)性概念的重要性。

顯而易見(jiàn),John 和 Ann 仍然連著,都有傳入的引用。但是,這樣還不夠。

前面說(shuō)的 "family" 對(duì)象已經(jīng)不再與根相連,沒(méi)有了外部對(duì)其的引用,所以它變成了一座“孤島”,并且將被從內(nèi)存中刪除。

內(nèi)部算法

垃圾回收的基本算法被稱為 “mark-and-sweep”。

定期執(zhí)行以下“垃圾回收”步驟:

  • 垃圾收集器找到所有的根,并“標(biāo)記”(記?。┧鼈儭?/li>
  • 然后它遍歷并“標(biāo)記”來(lái)自它們的所有引用。
  • 然后它遍歷標(biāo)記的對(duì)象并標(biāo)記 它們的 引用。所有被遍歷到的對(duì)象都會(huì)被記住,以免將來(lái)再次遍歷到同一個(gè)對(duì)象。
  • ……如此操作,直到所有可達(dá)的(從根部)引用都被訪問(wèn)到。
  • 沒(méi)有被標(biāo)記的對(duì)象都會(huì)被刪除。

例如,使我們的對(duì)象有如下的結(jié)構(gòu):


我們可以清楚地看到右側(cè)有一個(gè)“無(wú)法到達(dá)的島嶼”?,F(xiàn)在我們來(lái)看看“標(biāo)記和清除”垃圾收集器如何處理它。

第一步標(biāo)記所有的根:


然后,我們跟隨它們的引用標(biāo)記它們所引用的對(duì)象:


……如果還有引用的話,繼續(xù)標(biāo)記:


現(xiàn)在,無(wú)法通過(guò)這個(gè)過(guò)程訪問(wèn)到的對(duì)象被認(rèn)為是不可達(dá)的,并且會(huì)被刪除。


我們還可以將這個(gè)過(guò)程想象成從根溢出一大桶油漆,它流經(jīng)所有引用并標(biāo)記所有可到達(dá)的對(duì)象。然后移除未標(biāo)記的。

這是垃圾收集工作的概念。JavaScript 引擎做了許多優(yōu)化,使垃圾回收運(yùn)行速度更快,并且不會(huì)對(duì)代碼執(zhí)行引入任何延遲。

一些優(yōu)化建議:

  • 分代收集(Generational collection)—— 對(duì)象被分成兩組:“新的”和“舊的”。在典型的代碼中,許多對(duì)象的生命周期都很短:它們出現(xiàn)、完成它們的工作并很快死去,因此在這種情況下跟蹤新對(duì)象并將其從內(nèi)存中清除是有意義的。那些長(zhǎng)期存活的對(duì)象會(huì)變得“老舊”,并且被檢查的頻次也會(huì)降低。
  • 增量收集(Incremental collection)—— 如果有許多對(duì)象,并且我們?cè)噲D一次遍歷并標(biāo)記整個(gè)對(duì)象集,則可能需要一些時(shí)間,并在執(zhí)行過(guò)程中帶來(lái)明顯的延遲。因此,引擎將現(xiàn)有的整個(gè)對(duì)象集拆分為多個(gè)部分,然后將這些部分逐一清除。這樣就會(huì)有很多小型的垃圾收集,而不是一個(gè)大型的。這需要它們之間有額外的標(biāo)記來(lái)追蹤變化,但是這樣會(huì)帶來(lái)許多微小的延遲而不是一個(gè)大的延遲。
  • 閑時(shí)收集(Idle-time collection)—— 垃圾收集器只會(huì)在 CPU 空閑時(shí)嘗試運(yùn)行,以減少可能對(duì)代碼執(zhí)行的影響。

還有其他垃圾回收算法的優(yōu)化和風(fēng)格。盡管我想在這里描述它們,但我必須打住了,因?yàn)椴煌囊鏁?huì)有不同的調(diào)整和技巧。而且,更重要的是,隨著引擎的發(fā)展,情況會(huì)發(fā)生變化,所以在沒(méi)有真實(shí)需求的時(shí)候,“提前”學(xué)習(xí)這些內(nèi)容是不值得的。當(dāng)然,除非你純粹是出于興趣。我在下面給你提供了一些相關(guān)鏈接。

總結(jié)

主要需要掌握的內(nèi)容:

  • 垃圾回收是自動(dòng)完成的,我們不能強(qiáng)制執(zhí)行或是阻止執(zhí)行。
  • 當(dāng)對(duì)象是可達(dá)狀態(tài)時(shí),它一定是存在于內(nèi)存中的。
  • 被引用與可訪問(wèn)(從一個(gè)根)不同:一組相互連接的對(duì)象可能整體都不可達(dá),正如我們?cè)谏厦娴睦又锌吹降哪菢印?/li>

現(xiàn)代引擎實(shí)現(xiàn)了垃圾回收的高級(jí)算法。

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

如果你熟悉底層(low-level)編程,關(guān)于 V8 引擎垃圾回收器的更詳細(xì)信息請(qǐng)參閱文章 V8 之旅:垃圾回收。

V8 博客 還不時(shí)發(fā)布關(guān)于內(nèi)存管理變化的文章。當(dāng)然,為了學(xué)習(xí)更多垃圾收集的相關(guān)內(nèi)容,你最好通過(guò)學(xué)習(xí) V8 引擎內(nèi)部知識(shí)來(lái)進(jìn)行準(zhǔn)備,并閱讀一個(gè)名為 Vyacheslav Egorov 的 V8 引擎工程師的博客。我之所以說(shuō) “V8”,因?yàn)榫W(wǎng)上關(guān)于它的文章最豐富的。對(duì)于其他引擎,許多方法是相似的,但在垃圾收集上許多方面有所不同。

當(dāng)你需要底層的優(yōu)化時(shí),對(duì)引擎有深入了解將很有幫助。在熟悉了這門(mén)編程語(yǔ)言之后,把熟悉引擎作為下一步計(jì)劃是明智之選。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)