深入理解JS內(nèi)存管理機(jī)制

0

一、前言

在相當(dāng)長(zhǎng)一段時(shí)間里,JS運(yùn)行時(shí)的內(nèi)存問題都不被前端開發(fā)人員所關(guān)注。
一方面,日常開發(fā)中基本不會(huì)遇上需要對(duì)內(nèi)存精準(zhǔn)控制的場(chǎng)景,另一方面,寫JS不需要像寫 C/C++ 那樣在開發(fā)過程中隨時(shí)關(guān)注內(nèi)存的分配和釋放問題。

隨著生態(tài)的逐漸完善,JS的執(zhí)行環(huán)境也不再局限于瀏覽器中。目前,JS主要的執(zhí)行場(chǎng)景包括服務(wù)端(NodeJS、Deno)、桌面端(Electron)、瀏覽器(Chrome、Microsoft Edge)。

其中,JS執(zhí)行引擎 V8 因其優(yōu)異的性能表現(xiàn),已經(jīng)成為主流。因此,本文對(duì)JS內(nèi)存管理模型的研究也將基于V8展開。

二、內(nèi)存結(jié)構(gòu)

1

上圖展示了V8引擎的內(nèi)存結(jié)構(gòu),整體上分成兩部分:

2.1 堆內(nèi)存

這里是存儲(chǔ)對(duì)象或動(dòng)態(tài)數(shù)據(jù)的地方,也是占比最大的內(nèi)存區(qū)域。堆內(nèi)可以細(xì)分以下區(qū)域:

  • 新生代(Young generation) 新生代是新對(duì)象存在的地方,這些對(duì)象中的大多數(shù)都是短暫存在的。
    這部分空間很?。J(rèn)情況下 16~32M),并且拆分成了兩個(gè)空間。
    空間使用 Minor GC (Scavenger) 進(jìn)行垃圾回收。
  • 老生代(Old generation) 新生代中經(jīng)歷了兩個(gè)Minor GC 周期的對(duì)象會(huì)被轉(zhuǎn)移到老生代中存放。這里占據(jù)著大量的內(nèi)存空間(默認(rèn)情況下 700~1400M
    空間使用 Major GC(Mark-Sweep & Mark-Compact) 進(jìn)行垃圾回收。
  • 大對(duì)象區(qū)(LARGE OBJECT SPACE) 超過一定大小的對(duì)象會(huì)直接在大對(duì)象區(qū)中被創(chuàng)建,并在不被使用時(shí)將其直接回收。
  • 代碼區(qū)(CODE SPACE) 這是 即時(shí)編譯器(JIT) 存儲(chǔ)編譯代碼塊的地方。
  • 其他區(qū)(CELL, PROPERTY CELL,MAP SPACE) 這些空間存放大小相同的對(duì)象,并且對(duì)它們指向的對(duì)象類型有一些限制。
    比如MAP SPACE里存放的是hidden class信息,這能讓V8快速定位到對(duì)象值所在的內(nèi)存區(qū)。
2.2 棧內(nèi)存

棧是用來存儲(chǔ)靜態(tài)數(shù)據(jù)的地方,內(nèi)容主要包括:

  • 基本類型(Number, Boolean, String, Null, Undefined, Symbol, BigInt) 對(duì)于基本類型,系統(tǒng)會(huì)為新的變量在棧內(nèi)存中分配一個(gè)新值。
  • 引用類型 系統(tǒng)會(huì)為新的變量在棧內(nèi)存中分配一個(gè)值,這個(gè)值是一個(gè)對(duì)象的引用。
  • 調(diào)用棧 解釋器創(chuàng)建了調(diào)用棧來記錄函數(shù)的調(diào)用過程。
    每調(diào)用一個(gè)函數(shù),解釋器就把該函數(shù)添加進(jìn)調(diào)用棧,解釋器會(huì)為被添加進(jìn)來的函數(shù)創(chuàng)建一個(gè)棧幀(用來保存函數(shù)的局部變量以及執(zhí)行語(yǔ)句)并立即執(zhí)行。
    如果正在執(zhí)行的函數(shù)還調(diào)用了其他函數(shù),新函數(shù)會(huì)繼續(xù)被添加進(jìn)入調(diào)用棧。

三、內(nèi)存回收

棧內(nèi)存 其實(shí)是由操作系統(tǒng)進(jìn)行自動(dòng)管理的,本文不做討論。

堆內(nèi)存 由V8進(jìn)行管理,它占據(jù)最大的內(nèi)存空間,并且隨著程序運(yùn)行時(shí)間的增加可能會(huì)持續(xù)增長(zhǎng),最終耗盡內(nèi)存。它也會(huì)變得碎片化,影響程序運(yùn)行的速度。這時(shí)內(nèi)存回收的重要性就體現(xiàn)出來了。

要進(jìn)行內(nèi)存回收,需要先明確一個(gè)問題:什么樣的數(shù)據(jù)可以被回收。

V8通過回收 不可達(dá)對(duì)象 來釋放堆內(nèi)存,整個(gè)回收過程總體可以分為 標(biāo)記回收 兩個(gè)階段,涉及到的原理是 三色標(biāo)記分代回收

3.1 新生代

2.1堆內(nèi)存 小節(jié)中我們知道,新生代被拆分成兩個(gè)小空間,使用 Minor GC (Scavenger) 進(jìn)行垃圾回收。Minor GC (Scavenger) 我們可以簡(jiǎn)稱為次要GC。

兩個(gè)拆分出來的空間分別稱之為 to-spacefrom-space。新加入的對(duì)象都會(huì)存放到from-space,當(dāng)from-space被填滿時(shí),會(huì)觸發(fā)次要GC。

GC過程

  1. 標(biāo)記 從堆棧指針開始遞歸遍歷 from-space 中的對(duì)象圖查找 活躍對(duì)象。
  2. 復(fù)制 將這些對(duì)象復(fù)制到 to-space 中(包括被這些對(duì)象引用的所有對(duì)象)。
    重復(fù)此操作,直到掃描 from-space 中的所有對(duì)象。另外,to-space 會(huì)分配連續(xù)的內(nèi)存塊,以減少碎片。
  3. 清除 清空 from-space,因?yàn)榇藭r(shí)剩余的對(duì)象都是可回收的。
  4. 交換to-spacefrom-space 互換,即 to-space 變成 from-space。
image

回收的最后一步是更新引用已移動(dòng)的原始對(duì)象的指針。每個(gè)被復(fù)制的對(duì)象都會(huì)留下一個(gè)新地址,用于更新原始指針以指向新位置。

此時(shí)還存在一個(gè)問題:隨著活躍對(duì)象的累積,from-space 很快會(huì)被填滿。

這時(shí)就輪到老生代出場(chǎng)了,在新生代中經(jīng)歷兩次GC并存活下來的對(duì)象,會(huì)被轉(zhuǎn)移到老生代,這個(gè)過程被稱為晉升。如下圖:

image

至此,新生代一次完整的垃圾回收就完成了。

3.2 老生代

在老生代中,垃圾回收為主要GC(Major GC),包含了 標(biāo)記清除(Mark-Sweep)標(biāo)記整理(Mark-Compact)。

GC過程

  1. 標(biāo)記 垃圾收集器識(shí)別哪些對(duì)象正在使用,哪些對(duì)象未使用。正在使用或可從GC根域遞歸訪問的對(duì)象被標(biāo)記為活躍對(duì)象。
  2. 清除 清除未被標(biāo)記為活躍對(duì)象的數(shù)據(jù)。
  3. 整理 如果碎片較多,會(huì)將存活的對(duì)象移動(dòng)到一起,減少碎片提高內(nèi)存使用率。
2
3.3 三色標(biāo)記

之前提到,垃圾回收時(shí)會(huì)先標(biāo)記 活躍對(duì)象 來區(qū)分對(duì)象是否應(yīng)該被回收,這里就涉及到了三色標(biāo)記算法。

標(biāo)記位有三種顏色:

  • 白色 對(duì)象未被標(biāo)記。
  • 灰色 對(duì)象已經(jīng)被標(biāo)記,但對(duì)象內(nèi)屬性還未遍歷完成。
  • 黑色 對(duì)象已經(jīng)被標(biāo)記,且對(duì)象內(nèi)的屬性也已完成遍歷(活躍對(duì)象)。

標(biāo)記過程:

  1. 開始標(biāo)記
    初始所有對(duì)象都是白色,當(dāng)收集器發(fā)現(xiàn)白色對(duì)象并將其推送到標(biāo)記工作列表時(shí),會(huì)將其標(biāo)記成灰色。
    01.jpg
  1. 標(biāo)記完成對(duì)象
    當(dāng)收集器訪問目標(biāo)對(duì)象的所有字段后,會(huì)將對(duì)象的顏色由灰色變?yōu)楹谏?br>
    02.jpg
  1. 標(biāo)記結(jié)束
    當(dāng)沒有灰色對(duì)象時(shí),代表標(biāo)記結(jié)束。此時(shí)剩余的白色對(duì)象表示無法訪問,可以被回收。
    03.jpg

經(jīng)歷過以上三步,一次完整的標(biāo)記過程就完成了。

3.4 回收優(yōu)化

現(xiàn)在我們知道,一次垃圾回收總需要經(jīng)歷 標(biāo)記,回收,整理 等階段。

事實(shí)上,整個(gè)垃圾回收的過程是非常耗時(shí)的,比如光是標(biāo)記整個(gè)堆內(nèi)存的活躍對(duì)象可能就要花費(fèi)數(shù)百毫秒,并且期間會(huì)阻塞程序的正常的執(zhí)行。

[圖片上傳失敗...(image-ad8178-1632313567745)]

對(duì)此,V8也在持續(xù)優(yōu)化,目前主要的優(yōu)化方式有:

  • 增量標(biāo)記
    通過將標(biāo)記任務(wù)拆分成一系列小任務(wù),確保每次標(biāo)記任務(wù)的持續(xù)時(shí)間在 5~10 毫秒。
    當(dāng)堆的占用空間達(dá)到某個(gè)閾值大小時(shí),開始激活標(biāo)記任務(wù),此后每分配一定量的內(nèi)存,就會(huì)執(zhí)行增量標(biāo)記。
    增量標(biāo)記與常規(guī)標(biāo)記一樣,本質(zhì)上都是深度優(yōu)先搜索,同樣使用的是三色標(biāo)記算法。

  • 并行 & 并發(fā)

    • 并行 V8會(huì)創(chuàng)建輔助線程,與主線程同時(shí)處理GC任務(wù)。這樣GC時(shí)間就約等于總時(shí)間除以協(xié)同的線程數(shù)了。
    • 并發(fā) 這里的并發(fā)是指主線程不再處理GC任務(wù),而是由輔助線程來執(zhí)行。這樣的好處是垃圾回收不再阻塞正常任務(wù)的執(zhí)行。
  • 惰性清除
    當(dāng)所有對(duì)象都被標(biāo)記完成,此時(shí)已經(jīng)可以進(jìn)行垃圾清除的工作。但實(shí)際上這部分工作可以延遲執(zhí)行,尤其是當(dāng)內(nèi)存足夠的時(shí)候。
    V8會(huì)在合適的時(shí)間點(diǎn)執(zhí)行清除工作,比如工作線程空閑,或者內(nèi)存不足的時(shí)候。

總結(jié)

內(nèi)存管理是一件非常復(fù)雜的事情,本文主要從 內(nèi)存結(jié)構(gòu)內(nèi)存回收 兩個(gè)方面進(jìn)行了介紹,并且隱藏了其中的一些細(xì)節(jié)。

在V8的內(nèi)存管理模型中,其實(shí)能學(xué)習(xí)到一些通用的內(nèi)存管理思想和性能優(yōu)化方法。
比如,垃圾回收總是會(huì)圍繞標(biāo)記清除,整理展開。
在標(biāo)記算法上,V8JAVA,Golang,PHP 等編程語(yǔ)言一樣,使用了三色標(biāo)記。
在性能優(yōu)化上,多進(jìn)程/多線程, 分步, 異步, 延遲 等方式也總能發(fā)揮作用。


參考資料

原文已在玩物得志技術(shù)公眾號(hào)上發(fā)布,鏈接:https://mp.weixin.qq.com/s/7mvP5jv5sBGNfnThap6JSQ

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容