依托 V8 的 NODE 在使用內(nèi)存時是有大小限制的,具體大小與系統(tǒng)類型肥印、版本识椰、NODE 版本相關(guān)(64 位系統(tǒng) 1.4GB 和 32 位系統(tǒng) 0.7 GB 的大小)深碱。
NODE 與 C++不同腹鹉,垃圾回收 GC 由系統(tǒng)自動執(zhí)行,不由開發(fā)者參與莹痢,當(dāng)代碼使用不當(dāng)時种蘸,會導(dǎo)致 GC 不能正確回收發(fā)生內(nèi)存泄漏。
V8 對象(內(nèi)存)分配 及回收
> node --max-old-space-size=1700 test.js // 單位為MB竞膳,老生代內(nèi)存大小限制
> node --max-new-space-size=1024 test.js // 單位為KB航瞭,新生代內(nèi)存大小限制
>
> process.memoryUsage() // 查看內(nèi)存使用情況
< {
rss: 14958592,
heapTotal: 7195904, // 申請到的堆內(nèi)存
heapUsed: 2821496 // 使用的量
}
-
V8 內(nèi)存分代:新生代中的對象存活時間較短;老生代中的對象長駐或常駐內(nèi)存坦辟;
- 老生代在 64 位系統(tǒng)下默認大小限制為 1400 MB刊侯,在 32 位 700 MB
- 老生代在 32 位系統(tǒng)下默認大小限制為 32 MB,在 32 位 16 MB
-
回收算法:Scavenge 算法(新生代)锉走、Mark-Sweep & Mark-Compact(老生代兩者結(jié)合使用)
- Scavenge:將堆內(nèi)存一分為二滨彻,二者只有一個處理使用狀態(tài),稱為 From 空間挪蹭,空閑的為 To 空間亭饵。
- 分配對象在 From 空間。
- 垃圾回收時梁厉,釋放 From 中非活對象辜羊,復(fù)制存活對象到 To 空間。
- 完成復(fù)制后兩個空間的角色發(fā)生對換(又稱翻轉(zhuǎn))词顾。
- 晉升:當(dāng)一個對象多次復(fù)制后依然存活八秃,則移動到老生代中;或 To 空間使用超過 25%肉盹。
- Mark-Sweep:標(biāo)記活著的對象昔驱,回收沒有標(biāo)記的對象。(會造成空間不連續(xù))
- Mark-Compact:將活著的對象往一端移動上忍,移動完成后骤肛,清理掉邊界外的內(nèi)存纳本。
- Scavenge:將堆內(nèi)存一分為二滨彻,二者只有一個處理使用狀態(tài),稱為 From 空間挪蹭,空閑的為 To 空間亭饵。
-
回收執(zhí)行時機
- 全停頓 stop-the-world:GC 執(zhí)行時需要將應(yīng)用暫停,完成 GC 再恢復(fù)(時間代價較大)
- 增量標(biāo)記 incremental marking:(老生代)從標(biāo)記階段入手腋颠,拆分為小步饮醇,邏輯執(zhí)行與 GC 交替執(zhí)行
- 后續(xù)還引入了延遲清理(lazy sweeping)與增量式整理(incremental compaction),讓清理與整理動作也變成增量式的秕豫,進一步利用多核性能。
GC 耗時分析:Node 啟動時使用--prof 參數(shù)观蓄,可以得到 V8 執(zhí)行時的性能分析數(shù)據(jù)混移。但得到的日志文件不具備可讀性,需要借助工具(linux-tick-processor侮穿、windows-tick-processor.bat)
linux-tick-processor v8.log
來分析 GC 的耗時
作用域和閉包對內(nèi)存的影響
-
形成作用域:函數(shù)調(diào)用歌径、with、全局作用域
- 局部變量分配在作用域空間上亲茅,隨作用域回收而釋放回铛。如果變量小周期短會被分配到新生代的 Form 空間。
- 作用域鏈:在當(dāng)前作用域中無法找到變量的聲明克锣,將會向上級的作用域里查找茵肃,直到查到為止。
- 變量的主動釋放:=undefined 或 delete 對象袭祟。在 node10 中验残,對象和計算量多的情況下,執(zhí)行 =undefined 比 delete 大部分時間是快的巾乳,在 chrome90 中更明顯您没。
-
閉包:外部作用域訪問內(nèi)部作用域中變量的方法叫做閉包(closure)
- 比如 A 方法返回 B 函數(shù),B 訪問 A 中變量胆绊,a = A()氨鹏,只要 a 不被釋放 B 占用的內(nèi)存就得不到釋放。(a 不使用了及時=undefined压状,以釋放 B)
查看內(nèi)存使用情況
> process.memoryUsage() // 查看內(nèi)存使用情況
< {
rss: 14958592, // 常駐集大小仆抵,包括所有 C++ 和 JavaScript 對象和代碼;
heapTotal: 7195904, // 申請到的堆內(nèi)存
heapUsed: 2821496, // 使用的量
external: 13522, // 綁定到 V8 管理的 JavaScript 對象的 C++ 對象的內(nèi)存使用量何缓。
arrayBuffers: 15159 // 所有 Node.js Buffer
}
> os.totalmem() // 系統(tǒng)的總內(nèi)存
< 8589934592
> os.freemem() // 系統(tǒng)閑置內(nèi)存
< 185921536
- rss: resident set size 常駐集大小
- external:外部的肢础,堆外內(nèi)存
heapTotal 不包含 rss、arrayBuffers碌廓、external传轰。external 包含 arrayBuffers。批量操作 buffer 后谷婆,heapTotal慨蛙、heapUsed 不變辽聊,arrayBuffers、external 變大(rss 短時漲一些期贫,很快就下去了)
內(nèi)存泄漏
常見原因:意外的全局變量跟匆、沒有及時清理的計時器或回調(diào)、閉包
慎將內(nèi)存當(dāng)做緩存通砍,比如 store 中的大對象玛臂、已經(jīng)不用的變量,導(dǎo)致內(nèi)存泄漏甚至溢出封孙。存儲需求強烈可以使用 Redis 或 indexdb 等不占用 v8 緩存限制的方式迹冤。
日志收集時,寫入操作慢于日志產(chǎn)生虎忌,js 隊列數(shù)據(jù)過大導(dǎo)致內(nèi)存溢出泡徙,或記錄日志的 js 相關(guān)作用域得不到釋放,出現(xiàn)內(nèi)存泄漏膜蠢。
內(nèi)存泄漏排查工具
v8-profiler堪藐,可以用來分析 cpu(書中未詳說)
node-heapdump:
npm i heapdump
;在代碼的第一行添加如下代碼將其引入挑围;kill -USR2 PID
生成分析文件礁竞;導(dǎo)入 chrome 中的 Profiles 中進行分析,根據(jù)新生代(shallow size)杉辙、老生代(retained size)內(nèi)存占比推測泄漏的數(shù)據(jù)-
node-memwatch:
npm i memwatch
苏章;通過改變 heapDiff 的開始位置,或許可以逐步定位泄漏的位置奏瞬,通過 diff 結(jié)果可以推測泄漏的為數(shù)組memwatch.on("stats", cb) // 全堆垃圾回收 memwatch.on("leak", cb) // 內(nèi)存泄漏(如連續(xù)5次垃圾回收內(nèi)存仍未釋放) var hd = new memwatch.HeapDiff(); /* 要分析的代碼 */ ... var diff = hd.end(); // 內(nèi)容差異結(jié)果 /* { what: "String", size_bytes: 879424, size: "858.81 kb", +: 20001, // 分配的字符串對象數(shù)量 -: 1 // 釋放的字符串對象數(shù)量 } */
流(stream)或管道(pipe)專門用來操作需要大內(nèi)存的數(shù)據(jù)枫绅,且不受 V8 內(nèi)存限制的影響
純粹的 Buffer 操作(不涉及字符串),也不受 V8 內(nèi)存限制的影響