Node.js內(nèi)存管理機(jī)制分享

  1. Node.js內(nèi)存管理
  2. Node.js的一些選項(xiàng)
  3. GC研究例子
  4. 生產(chǎn)環(huán)境的設(shè)置

1. Node.js如何管理內(nèi)存

[解惑]
[GC與內(nèi)存泄露]
[V8程序員解釋V8 GC] [中文版]
[V8 GC加速]
[呃,想看點(diǎn)中文?]

簡(jiǎn)單概括一下,所有javascript(js)對(duì)象都放在V8管理的堆內(nèi)存(heap),heap分多個(gè)空間(space),查看有多少個(gè)space可以運(yùn)行一下的代碼

var v8 = require('v8')
console.log(v8.getHeapSpaceStatistics())

可以看到有new_space,old_space,code_space,map_space,large_object_space

另外buffer對(duì)象是放在堆外的,這又是另外一個(gè)故事,這里只說(shuō)一下跟GC相關(guān)的new_spaceold_space,分別被翻譯成新生代和老生代內(nèi)存空間。

一般來(lái)說(shuō)當(dāng)一個(gè)對(duì)象被創(chuàng)建的時(shí)候如果new space能放得下就不會(huì)放在large object space,
V8會(huì)定期運(yùn)行一種叫scavenge的高效算法去清理new space的內(nèi)存,當(dāng)一個(gè)對(duì)象經(jīng)歷了兩次scavengegc之后都沒(méi)有被釋放內(nèi)存那么就會(huì)被提升(promote)到old space。

old space的GC的算法叫mark and sweep,目前默認(rèn)是使用Incremental marking and lazy sweeping這樣一種增量的GC算法,目的是減少full gc帶來(lái)的停頓,變成每次gc一小部分內(nèi)存,每次gc停頓時(shí)間更小,但是gc次數(shù)會(huì)變多,減少大停頓時(shí)間的好處是在生產(chǎn)環(huán)境中能處理更多異步IO。

2. Node.js的一些選項(xiàng)

# 查看Node.js的啟動(dòng)選項(xiàng)
node -h
node --v8-options
  • --max-old-space-size可以限制V8里面的old space的最大值
  • --max-semi-space-size能限制new space里面的from或者to空間的最大值
  • --noincremental_marking是不使用增量markingGC算法的意思,開(kāi)了這個(gè)選項(xiàng)會(huì)讓old space內(nèi)存到達(dá)最大值的時(shí)候才進(jìn)行一次full gc
  • --trace-gc 能看到V8的gc日志

3. GC研究例子

以下是測(cè)試代碼

console.log(process.pid);

var arr = [];
var stringSize = 100000;
while(1) {
    for (var i = 0; i < 100; i++) {
        var s = new Array(stringSize).join('c');
        arr.push(s);
    }

    arr = [];
}

運(yùn)行環(huán)境:
macOS Sierra Version 10.12.3 8G RAM
Node.js v6.9.2

簡(jiǎn)單運(yùn)行,查看gc日志

node --trace-gc ggcc.js

輸入很多這些日志

[43370:0x102001000] Increasing marking speed to 3 due to high promotion rate
[43370:0x102001000]     2087 ms: Mark-sweep 41.4 (74.4) -> 5.0 (43.0) MB, 2.1 / 0.0 ms (+ 2.4 ms in 5 steps since start of marking, biggest step 1.2 ms) [GC interrupt] [GC in old space requested].

第二行的gc日志格式解讀

[pid] <time_since_start> : 
      <Phase> <heap_used_before (old+young)> (<allocated_heap_before>) ->
              <heap_used_after (old+young)> (<allocated_heap_after>) MB, 
              <time_spent_gc> [<reason_of_gc>]

可以看出,正常情況下運(yùn)行堆內(nèi)存(old + new space)保持在40M左右,GC后只有5M,因?yàn)閚ode v6默認(rèn)開(kāi)啟了增量掃描算法,所有會(huì)看到有類(lèi)似Increasing marking speed to 3 due to high promotion rate的日志輸出

改變 stringSize大小為10000

node --trace-gc ggcc.js

最后幾行輸出

[43844:0x102001600]     3870 ms: Scavenge 20.5 (45.0) -> 4.9 (45.0) MB, 0.2 / 0.0 ms [allocation failure].
[43844:0x102001600]     3875 ms: Scavenge 20.5 (45.0) -> 5.6 (45.0) MB, 0.6 / 0.0 ms [allocation failure].
[43844:0x102001600]     3881 ms: Scavenge 20.5 (45.0) -> 5.3 (45.0) MB, 0.3 / 0.0 ms [allocation failure].
[43844:0x102001600]     3886 ms: Scavenge 20.5 (45.0) -> 5.0 (45.0) MB, 0.2 / 0.0 ms [allocation failure].

可以看到,當(dāng)內(nèi)存不太吃緊的情況下,ScavengeGC算法就可以滿(mǎn)足內(nèi)存管理的需求

stringSize=100000,增加堆內(nèi)存最大限制

node --trace-gc --max-old-space-size=512 --max-semi-space-size=64 ggcc.js

最后幾行輸出

[44139:0x102001600] Increasing marking speed to 3 due to high promotion rate
[44139:0x102001600]     2828 ms: Mark-sweep 26.1 (157.0) -> 8.5 (141.0) MB, 2.4 / 0.0 ms (+ 2.8 ms in 9 steps since start of marking, biggest step 1.6 ms) [GC interrupt] [GC in old space requested].
[44139:0x102001600] Increasing marking speed to 3 due to high promotion rate
[44139:0x102001600]     2856 ms: Mark-sweep 34.3 (164.5) -> 11.3 (143.0) MB, 2.8 / 0.0 ms (+ 2.1 ms in 11 steps since start of marking, biggest step 1.1 ms) [GC interrupt] [GC in old space requested].

可以看到雖然限制了old和new space的最大內(nèi)存,但是因?yàn)殚_(kāi)啟了增量掃描算法,可以看到gc操作很是很頻繁的,堆內(nèi)存也穩(wěn)定在40M左右。

stringSize=100000,關(guān)閉增量掃描算法

node --trace-gc --max-old-space-size=512 --max-semi-space-size=128 --noincremental_marking ggcc.js

最后幾行輸出

[45098:0x102800c00]    11802 ms: Mark-sweep 563.1 (770.7) -> 11.2 (266.0) MB, 7.7 / 0.0 ms [allocation failure] [GC in old space requested].
[45098:0x102800c00]    12233 ms: Mark-sweep 567.6 (770.7) -> 5.9 (266.0) MB, 6.0 / 0.0 ms [allocation failure] [GC in old space requested].
[45098:0x102800c00]    12636 ms: Mark-sweep 562.4 (770.7) -> 10.5 (266.0) MB, 7.6 / 0.0 ms [allocation failure] [GC in old space requested].

從控制臺(tái)可以看出GC事件出現(xiàn)的頻率變少了,因?yàn)楝F(xiàn)在V8會(huì)等到old space內(nèi)存用盡了之后再進(jìn)行full gc。去掉old space大小限制再運(yùn)行會(huì)更明顯。


4. 生產(chǎn)環(huán)境的設(shè)置

因?yàn)?code>old space的默認(rèn)限制是1.7G,如果node.js運(yùn)行在低內(nèi)存的環(huán)境,有可能發(fā)生內(nèi)存不足從而kill掉自己(或者被操作系統(tǒng)kill掉)的情況,下面是開(kāi)了個(gè)內(nèi)存只有128兆的docker容器跑

docker run --memory="128m" --memory-swap="128m" -it hub.c.163.com/library/node:7.7.1 bash
root@dcd789a5a930:/# node --trace-gc --max-old-space-size=512 --max-semi-space-size=128 --noincremental_marking ggcc.js
[31:0x3e9ab00]       78 ms: Scavenge 3.3 (6.5) -> 2.9 (7.5) MB, 1.1 / 0.0 ms  allocation failure
31
[31:0x3e9ab00]      107 ms: Scavenge 5.8 (10.3) -> 5.8 (10.8) MB, 1.8 / 0.0 ms  allocation failure
[31:0x3e9ab00]      119 ms: Scavenge 11.8 (16.2) -> 11.8 (18.7) MB, 0.8 / 0.0 ms  allocation failure
[31:0x3e9ab00]      131 ms: Scavenge 23.0 (28.8) -> 23.0 (29.3) MB, 1.2 / 0.0 ms  allocation failure
[31:0x3e9ab00]      138 ms: Scavenge 29.0 (34.7) -> 29.0 (40.2) MB, 1.5 / 0.0 ms  allocation failure
[31:0x3e9ab00]      158 ms: Scavenge 57.4 (65.7) -> 57.4 (66.2) MB, 1.6 / 0.0 ms  allocation failure
[31:0x3e9ab00]      172 ms: Scavenge 63.4 (71.6) -> 63.3 (83.1) MB, 2.1 / 0.0 ms  allocation failure
Killed

從日志可以看到, Node.js并不會(huì)在內(nèi)存不足的時(shí)候主動(dòng)觸發(fā)GC,當(dāng)要求內(nèi)存太多的時(shí)候會(huì)被操作系統(tǒng)kill掉。

總結(jié)

通過(guò)對(duì)Node.js內(nèi)存管理機(jī)制的學(xué)習(xí),可以得出一下結(jié)論

  1. 小內(nèi)存環(huán)境下根據(jù)開(kāi)多少個(gè)Node.js進(jìn)程正確地設(shè)置對(duì)內(nèi)存大小限制,例如1G內(nèi)存的VPS主機(jī)建議設(shè)置 --max-old-space-size=768--max-semi-space-size=64,這樣只要沒(méi)有內(nèi)存泄露,Node.js的服務(wù)是可以正常運(yùn)行。
  2. 評(píng)價(jià)插件和商品搭配插件的機(jī)器是2G內(nèi)存的機(jī)器,建議這兩個(gè)服務(wù)都按照上述設(shè)置去運(yùn)行,因?yàn)閼岩蛇^(guò)評(píng)價(jià)插件有內(nèi)存泄漏的情況,但是一直找不出來(lái),可以先設(shè)置堆內(nèi)存使用上限再分析內(nèi)存泄漏的問(wèn)題。
  3. Node.js V4用的是V8 5.5版本,目前Node.js V6已經(jīng)是LTS版本而且用的是V8 5.5版本了,建議公有插件換用Node.js V6 LTS,更換錢(qián)要先通過(guò)測(cè)試。如果把上面實(shí)驗(yàn)的node版本換成V4,會(huì)有不同的結(jié)果,因?yàn)閚ode V4沒(méi)有--noincremental_marking選項(xiàng),默認(rèn)是有開(kāi)啟增量標(biāo)記算法的。

最后,用TJ的話(huà)總結(jié)一下這篇文章

Image
Image
最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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