JavaScript中V8引擎內存問題


簡介

V8 是谷歌開發的高性能 JavaScript 引擎,該引擎使用 C++ 開發。目前主要應用在 Google Chrome 瀏覽器和 node.js 當中。

V8 自帶的高性能垃圾回收機制,使開發者能夠專注於程序開發中,極大的提高開發者的編程效率。但是方便之余,也會出現一些對新手來說比較棘手的問題:進程內存暴漲,cpu 飆升,性能很差等。這個時候,了解 V8 的內存結構和垃圾回收機制、知道如何進行性能調優就很有必要。本文主要講述 V8 的內存管理和垃圾回收,后面會用示例代碼結合 Chrome 的開發者工具進行分析;最后介紹了阿里的 node.js 應用服務解決方案 alinode。

 

V8 內存構成

一個 V8 進程的內存通常由以下幾個塊構成:

  1. 新生代內存區(new space)大多數的對象都會被分配在這里,這個區域很小但是垃圾回收比較頻繁;
  2. 老生代內存區(old space)
    屬於老生代,這里只保存原始數據對象,這些對象沒有指向其他對象的指針;
  3. 大對象區(large object space)這里存放體積超越其他區大小的對象,每個對象有自己的內存,垃圾回收其不會移動大對象區;
  4. 代碼區(code space)
    代碼對象,會被分配在這里。唯一擁有執行權限的內存;
  5. map 區(map space)
    存放 Cell 和 Map,每個區域都是存放相同大小的元素,結構簡單。

內存構成可以用下圖來表示:

 

JavaScript中V8引擎內存問題

 

其中帶斜紋的是對應的內存塊中未使用的內存空間。new space 通常很小(1~8M),它被分成了兩部分,一部分叫做 inactive new space,一部分是激活狀態,為啥會有激活和未激活之分的原因,下面會提到。old space 偏大,可能達幾百兆。

 

V8 內存生命周期

假設代碼中有一個對象 jerry ,這個對象從創建到被銷毀,剛好走完了整個生命周期,通常會是這樣一個過程:

  1. 這個對象被分配到了 new space;
  2. 隨着程序的運行,new space 塞滿了,gc 開始清理 new space 里的死對象,jerry 因為還處於活躍狀態,所以沒被清理出去;
  3. gc 清理了兩遍 new space,發現 jerry 依然還活躍着,就把 jerry 移動到了 old space;
  4. 隨着程序的運行,old space 也塞滿了,gc 開始清理 old space,這時候發現 jerry 已經沒有被引用了,就把 jerry 給清理出去了。

第二步里,清理 new space 的過程叫做 Scavenge,這個過程采用了空間換時間的做法,用到了上面圖中的 inactive new space,過程如下:

  1. 當活躍區滿了之后,交換活躍區和非活躍區,交換后活躍區變空了;
  2. 將非活躍區的兩次清理都沒清理出去的對象移動到 old space;
  3. 將還沒清理夠兩次的但是活躍狀態的對象移動到活躍區。

第四步里,清理 old space 的過程叫做 Mark-sweep ,這塊占用內存很大,所以沒有使用 Scavenge,這個回收過程包含了若干次標記過程和清理過程:

  1. 標記從根(root)可達的對象為黑色;
  2. 遍歷黑色對象的鄰接對象,直到所有對象都標記為黑色;
  3. 循環標記若干次;
  4. 清理掉非黑色的對象。

簡單來說,Mark-sweep 就是把從根節點無法獲取到的對象清理掉了。

 

使用 Chrome 調優前端代碼

注:本文截圖里的 Chrome 版本為 Version 64.0.3282.140 (Official Build) (64-bit)

1. 查看內容構成

在控制台獲取當前頁面的堆內存快照(heap snapshot):

JavaScript中V8引擎內存問題

 

為了便於觀看,先在 console 里聲明一個類並創建它的一些對象:

class Jane {
}

class Tom {
  constructor () {  this.jane = new Jane()
  }
}

Array(1000000)  .fill('')   .map(() => new Tom())

 

獲取成功后,可以看到一個表格:

JavaScript中V8引擎內存問題

 

JavaScript中V8引擎內存問題

 

 

介紹一下幾個關鍵的列:

  1. Constructor:對象的類名;
  2. Distance:對象到根的引用層級;
  3. Objects Count:對象的數量;
  4. Shallow Size: 對象本身占用的內存,不包括引用的對象所占內存;
  5. Retained Size: 對象所占總內存,包含引用的其他對象所占內存;
  6. Retainers:對象的引用層級關系。

shallow size 和 retained size 的區別可以用紅框里的 Tom 和 Jane 更直觀的展示:Tom 的 shallow 占了 32M,retained 占用了 56M,這是因為 retained 包括了引用的指針對應的內存大小,即 tom.jane 所占用的內存;所以 Tom 的 retained 總和比 shallow 多出來的 24M 正好跟 Jane 占用的 24M 相同。retained size 可以理解為當回收掉該對象時可以釋放的內存大小,在內存調優中具有重要參考意義。

 

2. 查看對象的引用關系

這里使用一個稍復雜的代碼來展示:

class B {}

class A {
  constructor () {
    this.b = new B()
  }
}

class BList {
  constructor () {
    this.values = []
  }
  push (b) {
    this.values.push(b)
  }
}

const aArray = Array(1000000).fill('').map(() => new A())
const bList = new BList()
aArray.forEach(a => { bList.push(a.b) })
 

heap snapshot 如下圖所示:

JavaScript中V8引擎內存問題

 

 

紅框中展示了該 B 實例被應用的三個位置,后面的 @?? 可以視為內存的地址,同樣的地址意味着同一個對象。可以展開左邊的箭頭查看,這三個直接引用的地方分別是:

  1. Blist.values 對應的指針;
  2. A.b 對應的指針;
  3. Blist.values 指向的數組的指針。

可以觀察到,A 的 retained size 現在和 shallow size 一樣了,因為 A 的實例在 aArray 中被引用了;B 的兩個 size 也一樣了,因為在 A 中和 bList 中都有引用,銷毀其本身並不會釋放相應的內存。

 

3. 調試內存泄露

如果你的網頁在放久了的情況下內存越來越大甚至 tab 頁崩潰,那就要考慮是否內存泄露了。通過 Chrome 的任務管理器可以看到 JavaScript 所占用的內存:

JavaScript中V8引擎內存問題

 

通過 Performance 里的 record 也可以直觀地看到內存的增長(需要勾上 Memory 選項):

JavaScript中V8引擎內存問題

 

 

用一個示例代碼,結合 heap snapshot 來說明如何排查內存泄露:

 

const a = {}

setInterval(() => {
  a[Date.now()] = new ArrayBuffer(1000000)
}, 100)

 

這段代碼粘貼在控制台后,在控制台的 Memory 頁面,隔 10s 取一個 heap snapshot:

JavaScript中V8引擎內存問題

 

 

選中第二個和第三個,在選取觀察類型的下拉菜單里選擇「Comparison」,然后再選擇右面的下拉菜單,選擇上一個 snapshot:

JavaScript中V8引擎內存問題

 

 

JavaScript中V8引擎內存問題

 

這個時候后列表中的內容是當前的 snapshot 針對上一個的增加的部分,可以看到圖中的 snapshot 14 比 snapshot 13 多出來的部分,跟 snapshot 13 比 snapshot 12 多出來的部分都有 ArrayBuffer,那么就可以確定 ArrayBuffer 導致了內存泄露。這個時候可以結合上面一節的「查看對象引用關系」來定位到類或者代碼。

 

使用 alinode 調優 node.js 進程

alinode 是阿里雲出品的 node.js 應用服務解決方案,是一套基於社區 Node 改進的運行時環境和服務平台。使用 alinode 來調優 node.js 進程更加直觀,便捷,而且具備系統監控、日志服務、nodejs 進程監控、報警等功能,非常強大。

使用 alinode 要經過以下步驟:

  1. 注冊阿里雲賬號;
  2. 開通 alinode 服務;
  3. 創建 alinode 應用;
  4. 在自己服務器上安裝 alinode 和 agenthub,配置好自己應用的 id 和 secret;
  5. 啟動自己的 node 進程。

下圖是 alinode 某應用的一個實例的控制台:

 

JavaScript中V8引擎內存問題

 

 

圖中圈出來的是常用的幾個指標:

  1. 進程存活時間線:線斷了意味着可能進程重啟了或者機器網絡故障;
  2. CPU;
  3. 內存;
  4. GC 時間占比:一分鍾內 gc 占用時間的百分比;一般認為小於 5% 屬於正常狀態,比例很大的話意味着 CPU 需要耗費很多時間在 gc 上,導致進程性能嚴重下降。這通常對應以下三種情況:
    1. 進程負載太高,需要增加服務節點;
    2. 進程內存泄露,導致不停的在 gc;
    3. 代碼需要優化;
  5. CPU Profile:火焰圖的形式來統計分析;
  6. 堆快照:可以通過點擊「對快照」來生成 heap snapshot,可以下載下來通過 Chrome 來分析,也可以使用 alinode 自帶的分析工具:

 

JavaScript中V8引擎內存問題

 

 

此外還有一個專門針對內存和 gc 的工具:GC Trace,這個是用來觀察 gc 的過程,從而更直觀的觀察進程 gc 的步驟:

 

JavaScript中V8引擎內存問題

 

在這里,你可以直觀的看到堆大小的變化,可以看到每一次的 GC 是 Scavenge 還是 Mark-Sweep,可以看到每一次 gc 內存堆各類型的大小變化,有了這個強大的分析工具,你可以寫出性能更高、更加穩定、響應速度更快的 node.js 代碼。

 

作者:Tom Wan
鏈接:https://zhuanlan.zhihu.com/p/33816534
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

喜歡這篇文章?歡迎打賞~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM