banner
SlhwSR

SlhwSR

热爱技术的一名全栈开发者
github
bilibili

v8引擎垃圾回收原理

垃圾回收、內存優化#

我們知道 JS 通過堆和棧來存儲數據(分配內存)

  • 原始數據類型、代碼執行時棧幀的位置存儲在棧空間
  • 引用類型數據存儲在堆空間

但某些數據使用後不再需要,就變成了垃圾數據,如果不回收,內存佔用就會越來越多
一般垃圾回收分為手動回收自動回收兩種策略

像 C/C++ 是使用手動回收策略,需要代碼來決定如何分配和銷毀內存,而 Javascript/Java/Python 等語言都由垃圾回收期來自動釋放內存,但並不代表我們不需要關心內存管理(特別是 JS,很多開發很容易被自動垃圾回收迷惑,從而忽略內存管理)

一、棧內存回收#

先看調用棧中的數據是如何回收的,從執行上下文開始

當一個函數執行上下文被創建並入棧,元素類型數據分配到棧,引用類型數據分配到堆。

與此同時,有一個記錄當前執行狀態的指針(ESP),執行當前的執行上下文 A

當函數 A 調用完畢出棧時,ESP 會下移到下一個執行上下文 B,這個下移操作就是銷毀上一個執行上下文 A 的過程,雖然 A 的執行上下文還保存在棧內存中,但已經是無效內存了,下次再調用其他函數時,會直接覆蓋這塊內存,用來存放其他函數的執行上下文

image

所以,當一個函數執行結束之後,JS 引擎會通過向下移動 ESP 來銷毀該函數的執行上下文

二、堆內存回收#

函數在棧中的執行上下文被銷毀後,那還有一些保存在堆中的對象怎麼處理呢,那就需要用到垃圾回收器了

2.1 分代收集與主要流程#

V8 中,會把堆分為新生代和老生代兩個區域

  • 新生代:存放生存時間短的對象 -> 副垃圾回收器 Minor GC
  • 老生代:存放生存時間久的對象 -> 主垃圾回收器 Major GC

垃圾回收器有一套統一執行流程

  • 標記對象
  • 活動對象:還在使用的對象
  • 非活動對象:可以進行垃圾回收
  • 回收非活動對象佔據的內存
  • 內存整理:整理內存碎片,留出連續空間,方便後續分配較大連續內存

2.2 副垃圾回收器#

image

新加入的對象都會加入到對象區,對象區塊寫滿時,需要執行一次垃圾回收

對象區域中的垃圾做標記 -> 存活的對象複製到空閒區域中 -> 有序排列 -> 空閒區域沒有垃圾碎片 -> 對象區和空閒區的概念發生翻轉 -> 無限重複利用

副垃圾回收器一旦監控對象裝滿了,便執行垃圾回收。同時,副垃圾回收器還會採用對象晉升策略,也就是移動那些經過兩次垃圾回收依然還存活的對象到老生代中。

2.3 主垃圾回收器#

老生代堆裡的對象特點

  • 一個是對象佔用空間大
  • 另一個是對象存活時間長

主垃圾回收器採用的算法有兩種

  • 標記 - 清除(Mark-Sweep)
  • 標記 - 整理(Mark-Compact)

JS 裡還有一種最初級的垃圾回收算法:引用計數,如果沒有引用指向該對象,則被回收,但該算法的缺陷是無法處理循環引用的問題

先對垃圾數據進行標記。標記階段就是從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數據。

然後清除,這就是標記 - 清除算法

image

不過對一塊內存多次執行標記 - 清除算法後,會產生大量不連續的內存碎片。而碎片過多會導致大對象無法分配到足夠的連續內存,於是又引入了另外一種算法 —— 標記 - 整理(Mark-Compact)。

先標記可回收對象,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉這一端之外的內存

image

2.4 垃圾回收小結#

image

三、垃圾回收的效率優化#

JavaScript 是運行在主線程之上的,因此,一旦執行垃圾回收算法,都需要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行。這種行為叫做全停頓(Stop-The-World)。

image

第一,將一個完整的垃圾回收的任務拆分成多個小的任務,這樣就消滅了單個長的垃圾回收任務;
第二,將標記對象、移動對象等任務轉移到後台線程進行,這會大大減少主線程暫停的時間,改善頁面卡頓的問題,讓動畫、滾動和用戶交互更加流暢。

3.1 優化方案#

  • 第一個方案是並行回收,在執行一個完整的垃圾回收過程中,垃圾回收器會使用多個輔助線程來並行執行垃圾回收。
  • 第二個方案是增量式垃圾回收,垃圾回收器將標記工作分解為更小的塊,並且穿插在主線程不同的任務之間執行。採用增量垃圾回收時,垃圾回收器沒有必要一次執行完整的垃圾回收過程,每次執行的只是整個垃圾回收過程中的一小部分工作。
  • 第三個方案是並發回收,輔助線程在執行垃圾回收的時候,主線程也可以自由執行而不會被掛起,難點在於讀寫鎖機制(這裡不深入了)

主垃圾回收器就綜合採用了所有的方案,副垃圾回收器也採用了部分方案。

image

四、內存優化#

常見三類內存問題

  • 內存泄漏 (Memory leak),它會導致頁面的性能越來越差;
    • 在 JavaScript 中,造成內存泄漏 (Memory leak) 的主要原因是不再需要 (沒有作用) 的內存數據依然被其他對象引用著。
  • 內存膨脹 (Memory bloat),它會導致頁面的性能會一直很差;
  • 頻繁垃圾回收,它會導致頁面出現延遲或者經常暫停。

4.1 內存泄漏常見原因和解決方案#

  • 全局變量:儘量避免設置比較大的全局對象
  • 監聽器、定時器引用沒有及時清除
  • 閉包常駐內存:比較容易形成對象的循環引用,把變量設置 null 切斷聯繫
  • DOM 引用:保留了 DOM 節點的引用,導致 GC 沒有回收

除了以上,多用 ES6 的WeakMap/WeakSet,它們都保持對象的弱引用

4.2 內存泄漏識別方法#

  • 在瀏覽器中可通過查看 memory 來觀察內存是否有上升的趨勢
  • 在 Node 環境可通過 headdump 抓取內存信息進行分析

參考#

Orinoco — 新的 V8 垃圾回收器 by Peter Marshal

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。