JavaScript 內存管理及 V8 優化

JavaScript 內存管理

JavaScript 具備垃圾自動回收機制(Garbage Collection)簡稱 GC。垃圾回收機制會中斷整個代碼執行,釋放不可能再被使用的變量,釋放內存,這個工做機制是週期性的,咱們會在下文詳細探討。javascript

可釋放對象

function fn1() {
  var obj1 = { name: 'xiaomuchen', age: '20' }
}
function fn2() {
  var obj2 = { name: 'xiaomuchen', age: '20' }
  return obj2
}
var a = fn1()
var b = fn2()
console.log(a, b) // undefined, {name: "xiaomuchen", age: "20"}

咱們對比上面兩個函數,fn1 在函數內聲明變量 obj1 而且賦值,在函數執行後這個變量便不可再訪問了,fn2 在最後把函數內的變量 obj2 返回到全局變量 b,因此 { name: 'xiaomuchen', age: '20' } 這個對象(或者說 obj2)依然可被訪問html

JavaScript 回收機制經過判斷變量是否可被訪問,來決定回收哪些變量。java

標記清除和引用計數

那麼 JavaScript 是如何判斷變量是否可被訪問?這就要提到標記清除和引用計數。node

標記清除:標記清除是目前大部分 JavaScript 引擎使用的判斷方式,經過標記變量的狀態來肯定是否可被回收。當變量在環境中被聲明時標記進入環境,理論上永遠不要釋放進入環境的變量,由於它能夠在環境中的任何位置、任什麼時候刻被訪問。當環境被銷燬(如函數執行完),則變量被標記離開環境等待回收。git

function fn(){
  var a = { count: 10 } // 被標記,進入環境 
  var b = { count: 20 } // 被標記,進入環境
}
fn(); // 執行完畢以後 b 被標記,離開環境

引用計數:JavaScript 引擎維護一張引用表,保存內存中全部的資源的引用次數。資源被引用一次則引用 +1,資源被去掉引用或者退出變量的函數做用域時,則引用 -1,當資源的引用次數爲0時,說明沒法訪問這個值,則等待回收。
(注:引用計數從 1 到 0 這個過程可能不執行,而是直接標記可被回收,再也不進行加減運算節約開銷)github

function fn(){
  var a = { count: 10 } // 資源 { count: 10 } 被引用次數爲 1
  a = { count: 20 } // 資源 { count: 20 } 被引用次數爲 1,資源 { count: 10 } 被引用次數爲 0,等待回收
  // do someThing
}
fn(); // 資源 { count: 20 } 被釋放

可是引用計數存在一種循環引用的狀況,以下例子,兩個對象之間相互引用,在離開環境後對象不可訪問,但因爲對象的引用次數爲 1,則致使不會被回收。這個例子來自《JavaScript 高級程序設計》,但我思考良久,若是引用計數把 a.param 也做爲一個變量來計數,那麼就沒有這個問題了,引用計數實現的方式不一樣,產生的結果也不同。服務器

function fn(){
  var a = { count: 10 }
  var b = { count: 20 }
  a.param = b // b 的引用次數爲 2
  b.param = a // a 的引用次數爲 2
}
fn(); // a、b 的引用次數爲 1

GC 的缺陷、分代回收和增量 GC

和其餘語言同樣 GC 會中斷代碼執行,中止其餘操做。由於要遍歷全部對象,回收全部不可訪問對象,這個操做的耗時可能有 100ms 以上。在 V8 引擎新版本中引入了兩種優化方法:1. 分代回收(Generation GC),2. 增量 GC(increment GC)網絡

分代回收:目的是經過對象的使用頻率、存在時長區分新生代與老生代對象。多回收新生代區(young generation),少回收老生代區(tenured generation),減小每次需遍歷的對象,從而減小每次GC的耗時閉包

增量 GC:把須要長耗時的遍歷、回收操做拆分運行,減小中斷時間,可是會增大上下文切換開銷函數

Node.js 中的 GC 表現

當咱們用 Node.js 搭建一個穩定的服務時,就須要考慮服務器內存的開銷,下面一個 Node.js 內存回收執行的例子:

執行代碼node --trace_gc --trace_gc_verbose test.js跟蹤一個網絡服務的 GC。

[41204:0x102001c00] Memory reducer: call rate 0.056, low alloc, foreground
[41204:0x102001c00] Memory reducer: started GC #1
[41204:0x102001c00] Heap growing factor 1.1 based on mu=0.970, speed_ratio=42956 (gc=675253, mutator=16)
[41204:0x102001c00] Grow: old size: 21382 KB, new limit: 33604 KB (1.1)
[41204:0x102001c00] Memory reducer: finished GC #1 (will do more)
[41204:0x102001c00]   156410 ms: Mark-sweep 27.7 (50.0) -> 21.0 (30.0) MB, 12.4 / 0.0 ms (+ 20.4 ms in 7 steps since start of marking, biggest step 4.8 ms) [Incremental marking task: finalize incremental marking] [GC in old space requested].
[41204:0x102001c00] Memory allocator,   used:  30756 KB, available: 1435612 KB
[41204:0x102001c00] New space,          used:    169 KB, available:    838 KB, committed:   1024 KB
[41204:0x102001c00] Old space,          used:  16662 KB, available:   2417 KB, committed:  19412 KB
[41204:0x102001c00] Code space,         used:   4078 KB, available:    178 KB, committed:   5120 KB
[41204:0x102001c00] Map space,          used:    642 KB, available:      0 KB, committed:   2128 KB
[41204:0x102001c00] Large object space, used:      0 KB, available: 1434571 KB, committed:      0 KB
[41204:0x102001c00] All spaces,         used:  21552 KB, available: 1438005 KB, committed:  27684 KB
[41204:0x102001c00] External memory reported:   1026 KB
[41204:0x102001c00] Total time spent in GC  : 158.6 ms
[41204:0x102001c00] Memory reducer: call rate 0.003, low alloc, foreground

首先咱們能夠看到 Node.js 區分 New spaceOld space 等來劃分檢索空間。而提示(+ 20.4 ms in 7 steps since start of marking, biggest step 4.8 ms) 告訴咱們這個標記的步驟分 7 步進行,耗時最長的一次時 4.8ms。這使 JavaScript 能夠很好的支持開發高實時應用,原文

總結

由於篇幅有限,留下一些小問題供你們思考:

  1. 閉包必定會致使內存不可被回收?
  2. 如何監控一個 Node.js 服務的內存開銷,如何處理不可預知的內存泄漏?

做者:肖沐宸,github

相關文章
相關標籤/搜索