簡介
V8 是谷歌開發的高性能 JavaScript 引擎,該引擎使用 C++ 開發。目前主要應用在 Google Chrome 瀏覽器和 node.js 當中。
V8 自帶的高性能垃圾回收機制,使開發者能夠專注於程序開發中,極大的提高開發者的編程效率。但是方便之余,也會出現一些對新手來說比較棘手的問題:進程內存暴漲,cpu 飆升,性能很差等。這個時候,了解 V8 的內存結構和垃圾回收機制、知道如何進行性能調優就很有必要。本文主要講述 V8 的內存管理和垃圾回收,后面會用示例代碼結合 Chrome 的開發者工具進行分析;最后介紹了阿里的 node.js 應用服務解決方案 alinode。
V8 內存構成
一個 V8 進程的內存通常由以下幾個塊構成:
- 新生代內存區(new space)大多數的對象都會被分配在這里,這個區域很小但是垃圾回收比較頻繁;
- 老生代內存區(old space)
屬於老生代,這里只保存原始數據對象,這些對象沒有指向其他對象的指針; - 大對象區(large object space)這里存放體積超越其他區大小的對象,每個對象有自己的內存,垃圾回收其不會移動大對象區;
- 代碼區(code space)
代碼對象,會被分配在這里。唯一擁有執行權限的內存; - map 區(map space)
存放 Cell 和 Map,每個區域都是存放相同大小的元素,結構簡單。
內存構成可以用下圖來表示:
其中帶斜紋的是對應的內存塊中未使用的內存空間。new space 通常很小(1~8M),它被分成了兩部分,一部分叫做 inactive new space,一部分是激活狀態,為啥會有激活和未激活之分的原因,下面會提到。old space 偏大,可能達幾百兆。
V8 內存生命周期
假設代碼中有一個對象 jerry ,這個對象從創建到被銷毀,剛好走完了整個生命周期,通常會是這樣一個過程:
- 這個對象被分配到了 new space;
- 隨着程序的運行,new space 塞滿了,gc 開始清理 new space 里的死對象,jerry 因為還處於活躍狀態,所以沒被清理出去;
- gc 清理了兩遍 new space,發現 jerry 依然還活躍着,就把 jerry 移動到了 old space;
- 隨着程序的運行,old space 也塞滿了,gc 開始清理 old space,這時候發現 jerry 已經沒有被引用了,就把 jerry 給清理出去了。
第二步里,清理 new space 的過程叫做 Scavenge,這個過程采用了空間換時間的做法,用到了上面圖中的 inactive new space,過程如下:
- 當活躍區滿了之后,交換活躍區和非活躍區,交換后活躍區變空了;
- 將非活躍區的兩次清理都沒清理出去的對象移動到 old space;
- 將還沒清理夠兩次的但是活躍狀態的對象移動到活躍區。
第四步里,清理 old space 的過程叫做 Mark-sweep ,這塊占用內存很大,所以沒有使用 Scavenge,這個回收過程包含了若干次標記過程和清理過程:
- 標記從根(root)可達的對象為黑色;
- 遍歷黑色對象的鄰接對象,直到所有對象都標記為黑色;
- 循環標記若干次;
- 清理掉非黑色的對象。
簡單來說,Mark-sweep 就是把從根節點無法獲取到的對象清理掉了。
使用 Chrome 調優前端代碼
注:本文截圖里的 Chrome 版本為 Version 64.0.3282.140 (Official Build) (64-bit)
1. 查看內容構成
在控制台獲取當前頁面的堆內存快照(heap snapshot):
為了便於觀看,先在 console 里聲明一個類並創建它的一些對象:
class Jane { } class Tom { constructor () { this.jane = new Jane() } } Array(1000000) .fill('') .map(() => new Tom())
獲取成功后,可以看到一個表格:
介紹一下幾個關鍵的列:
- Constructor:對象的類名;
- Distance:對象到根的引用層級;
- Objects Count:對象的數量;
- Shallow Size: 對象本身占用的內存,不包括引用的對象所占內存;
- Retained Size: 對象所占總內存,包含引用的其他對象所占內存;
- 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 如下圖所示:
紅框中展示了該 B 實例被應用的三個位置,后面的 @?? 可以視為內存的地址,同樣的地址意味着同一個對象。可以展開左邊的箭頭查看,這三個直接引用的地方分別是:
- Blist.values 對應的指針;
- A.b 對應的指針;
- Blist.values 指向的數組的指針。
可以觀察到,A 的 retained size 現在和 shallow size 一樣了,因為 A 的實例在 aArray 中被引用了;B 的兩個 size 也一樣了,因為在 A 中和 bList 中都有引用,銷毀其本身並不會釋放相應的內存。
3. 調試內存泄露
如果你的網頁在放久了的情況下內存越來越大甚至 tab 頁崩潰,那就要考慮是否內存泄露了。通過 Chrome 的任務管理器可以看到 JavaScript 所占用的內存:
通過 Performance 里的 record 也可以直觀地看到內存的增長(需要勾上 Memory 選項):
用一個示例代碼,結合 heap snapshot 來說明如何排查內存泄露:
const a = {} setInterval(() => { a[Date.now()] = new ArrayBuffer(1000000) }, 100)
這段代碼粘貼在控制台后,在控制台的 Memory 頁面,隔 10s 取一個 heap snapshot:
選中第二個和第三個,在選取觀察類型的下拉菜單里選擇「Comparison」,然后再選擇右面的下拉菜單,選擇上一個 snapshot:
這個時候后列表中的內容是當前的 snapshot 針對上一個的增加的部分,可以看到圖中的 snapshot 14 比 snapshot 13 多出來的部分,跟 snapshot 13 比 snapshot 12 多出來的部分都有 ArrayBuffer,那么就可以確定 ArrayBuffer 導致了內存泄露。這個時候可以結合上面一節的「查看對象引用關系」來定位到類或者代碼。
使用 alinode 調優 node.js 進程
alinode 是阿里雲出品的 node.js 應用服務解決方案,是一套基於社區 Node 改進的運行時環境和服務平台。使用 alinode 來調優 node.js 進程更加直觀,便捷,而且具備系統監控、日志服務、nodejs 進程監控、報警等功能,非常強大。
使用 alinode 要經過以下步驟:
- 注冊阿里雲賬號;
- 開通 alinode 服務;
- 創建 alinode 應用;
- 在自己服務器上安裝 alinode 和 agenthub,配置好自己應用的 id 和 secret;
- 啟動自己的 node 進程。
下圖是 alinode 某應用的一個實例的控制台:
圖中圈出來的是常用的幾個指標:
- 進程存活時間線:線斷了意味着可能進程重啟了或者機器網絡故障;
- CPU;
- 內存;
- GC 時間占比:一分鍾內 gc 占用時間的百分比;一般認為小於 5% 屬於正常狀態,比例很大的話意味着 CPU 需要耗費很多時間在 gc 上,導致進程性能嚴重下降。這通常對應以下三種情況:
- 進程負載太高,需要增加服務節點;
- 進程內存泄露,導致不停的在 gc;
- 代碼需要優化;
- CPU Profile:火焰圖的形式來統計分析;
- 堆快照:可以通過點擊「對快照」來生成 heap snapshot,可以下載下來通過 Chrome 來分析,也可以使用 alinode 自帶的分析工具:
此外還有一個專門針對內存和 gc 的工具:GC Trace,這個是用來觀察 gc 的過程,從而更直觀的觀察進程 gc 的步驟:
在這里,你可以直觀的看到堆大小的變化,可以看到每一次的 GC 是 Scavenge 還是 Mark-Sweep,可以看到每一次 gc 內存堆各類型的大小變化,有了這個強大的分析工具,你可以寫出性能更高、更加穩定、響應速度更快的 node.js 代碼。
作者:Tom Wan
鏈接:https://zhuanlan.zhihu.com/p/33816534
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
喜歡這篇文章?歡迎打賞~~

