作者 | 后端Team朱捷峰
整理 | 包包
V8 垃圾回收機(jī)制
事實(shí)上,我們平時(shí)在寫(xiě) Node.js 的時(shí)候很少去關(guān)心內(nèi)存問(wèn)題,那是因?yàn)?Node.js 對(duì) Google V8 進(jìn)行封裝,底層的垃圾收回機(jī)制都交給 V8 處理。大部分時(shí)候,是不會(huì)有內(nèi)存問(wèn)題的。相對(duì)于 C/C++ 這類(lèi)需要自己管理內(nèi)存的語(yǔ)言,Node.js 有更加平滑的學(xué)習(xí)曲線,這也是 Node.js 最大的優(yōu)勢(shì)之一。但是也總有意外情況,可能導(dǎo)致 Node.js 進(jìn)程內(nèi)存泄漏。
那么如何避免我們的 Node.js 程序出現(xiàn)內(nèi)存泄漏的情況呢?我們先來(lái)了解下 V8 內(nèi)存管理機(jī)制。
一個(gè)進(jìn)程通常是通過(guò)在內(nèi)存中分配空間來(lái)體現(xiàn)的,這個(gè)空間我們稱(chēng)之為 Resident Set(常駐空間)。V8 將內(nèi)存分為了以下幾塊:
? 代碼區(qū):實(shí)際正在運(yùn)行的代碼
? 棧區(qū):包含了所有的值類(lèi)型(數(shù)字、布爾值等)、指向存儲(chǔ)在堆區(qū)的對(duì)象指針、定義程序控制流的指針
? 堆區(qū):專(zhuān)門(mén)用來(lái)存儲(chǔ)引用類(lèi)型的內(nèi)存區(qū)域,比如對(duì)象、字符串和閉包
在 Node.js 中,我們可以通過(guò)調(diào)用process.memoryUsage() 方法來(lái)來(lái)查詢內(nèi)存使用情況。該函數(shù)返回值如下:
memory usage
{
rss: 4935680,
heapTotal: 1826816,
heapUsed: 650472,
external: 49879
}
以上數(shù)值以字節(jié)為單位
? rss:表示 Resident Set 的大小
? heapTotal:表示堆的總大小
? heapUsed:表示堆的實(shí)際使用大小
? external:表示 V8 管理的綁定到 JavaScript 對(duì)象的 C++ 對(duì)象的大小
我們知道在 Node.js 的運(yùn)行時(shí)中,JavaScript 是由 V8 編譯成可執(zhí)行的機(jī)器碼。運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu)是由 V8 來(lái)管理的,我們能做的很有限。通過(guò) JavaScript 我們是沒(méi)法做到分配內(nèi)存和釋放內(nèi)存的。
V8 的垃圾回收算法實(shí)現(xiàn)還是很復(fù)雜的,感興趣的同學(xué)可以參考:http://newhtml.net/v8-garbage-collection/。但是我們?nèi)匀豢梢园言砗?jiǎn)單抽象:如果一個(gè)內(nèi)存片段沒(méi)有被任何地方引用,我們可以假設(shè)它不再會(huì)被用到,那么該內(nèi)存片段可以被釋放。
上圖表示在內(nèi)存中各個(gè)對(duì)象的引用情況,只有當(dāng)紅球?qū)ο蟛辉俦蝗魏螌?duì)象引用的時(shí)候,它才能被回收。
異常情況
既然 V8 會(huì)進(jìn)行垃圾回收,那我們?yōu)槭裁催€要關(guān)心內(nèi)存情況呢?
理想情況,內(nèi)存占用會(huì)保持在一個(gè)相對(duì)穩(wěn)定的范圍:
實(shí)際上,我們?nèi)匀豢赡軙?huì)看到內(nèi)存占用升高的情況:
V8 垃圾回收機(jī)制盡可能地回收和釋放內(nèi)存,但是每次執(zhí)行垃圾回收以后,內(nèi)存占用仍然持續(xù)上升,這明顯就是內(nèi)存泄漏了。
制造內(nèi)存泄漏
有一些很明顯的情況會(huì)導(dǎo)致內(nèi)存泄漏:1、比如將每位訪客的 IP 記錄在 global 上存儲(chǔ)數(shù)組上;2、再比如著名的“ 沃爾瑪內(nèi)存泄漏事件”,它是由 Node.js 核心代碼中一個(gè)遺漏的聲明引發(fā)的血案,工程師們花了好幾個(gè)星期去排查并最終得以解決。
在這篇文章里,我們就不一一列舉所有可能產(chǎn)生問(wèn)題的錯(cuò)誤情況。我們來(lái)看一下一個(gè)難以排查的情況,代碼很簡(jiǎn)單,你可以自己運(yùn)行調(diào)試:
memory leak demo
const express = require('express');
const app = express();
const port = 3000;
let theThing = null;
const replaceThing = function () {
let originalThing = theThing;
let unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
app.get('/leak', (req, res) => {
replaceThing();
let memoryInfo = JSON.stringify(process.memoryUsage());
console.log(memoryInfo);
res.send(memoryInfo);
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
初看的話,這段代碼沒(méi)啥問(wèn)題。我們可以想象 theThing 在每次調(diào)用 replaceThing() 時(shí)會(huì)被重寫(xiě)。問(wèn)題就在于,someMethod 有閉包作用域作為上下文,這就意味著當(dāng)調(diào)用 someMethod 時(shí),unused 是可見(jiàn)的。雖然實(shí)際上 unused 并沒(méi)有被調(diào)用,但是它卻阻止了 V8 垃圾回收機(jī)制對(duì) originalThing 進(jìn)行回收。這就是我們平時(shí)所說(shuō)的“循環(huán)引用”:
既然找到問(wèn)題所在,那么如何解決呢?答案很簡(jiǎn)單,我們只要切斷循環(huán)引用就可以了,這里我們只需要在 replaceThing 這個(gè)方法最后加入 theThing = null。
針對(duì)這個(gè)問(wèn)題,我們還可以通過(guò) ESLint 的 no-unused-vars 規(guī)則來(lái)避免定義了但是未使用的變量,這樣可以減少循環(huán)引用的可能性。
排查問(wèn)題
理解了垃圾回收的原理,那么我們平常在碼代碼的時(shí)候也要注意避免循環(huán)引用的情況出現(xiàn)。但是就像上面這種情況,有時(shí)候就是防不勝防。那么遇到問(wèn)題的時(shí)候,我們應(yīng)該如何排查呢?
推薦一下我寫(xiě)的一個(gè)小工具 heapsnapshot.js ,可以獲取生成堆的快照信息,如下圖:
然后利用 Chrome 開(kāi)發(fā)者工具,Memory 來(lái)做具體分析:
請(qǐng)選擇相鄰的3個(gè)堆快照文件,導(dǎo)入 Memory 分析工具中,如下圖:
第一步,先選擇 Profiles 中的第二個(gè)文件,然后篩選 Objects 選項(xiàng)選擇“Objects allocated between 1539255057342 and 1539255076968 ”,然后在 Constructor 中進(jìn)行具體的分析 。
第二步,同理對(duì)第二個(gè)和第三個(gè)文件進(jìn)行對(duì)比分析。找到兩次分析都出現(xiàn)過(guò)的元素,重點(diǎn)排查,定位到具體的問(wèn)題代碼,再做修改。
第三步,重復(fù)上述過(guò)程,檢查內(nèi)存泄漏問(wèn)題是否解決。
以上只是對(duì) Node.js 內(nèi)存問(wèn)題的一個(gè)初步探討,感興趣的話推薦大家去看下 V8 垃圾回收的原理。平常我們?cè)诰幋a的時(shí)候也要注意盡量避免產(chǎn)生循環(huán)引用,但是如果遇到了也不要擔(dān)心,可以通過(guò)上面的步驟排查解決。
-
內(nèi)存
+關(guān)注
關(guān)注
9文章
3174瀏覽量
76166 -
NODE.JS
+關(guān)注
關(guān)注
1文章
49瀏覽量
33912
發(fā)布評(píng)論請(qǐng)先 登錄
WebGL/Canvas 內(nèi)存泄露分析
at_device 包 ml307長(zhǎng)時(shí)間運(yùn)行有內(nèi)存泄漏問(wèn)題怎么解決?
【M-K1HSE開(kāi)發(fā)板免費(fèi)體驗(yàn)】M-K1HSE開(kāi)發(fā)板構(gòu)建HELLO WORLD頁(yè)面
在OpenVINO? C++代碼中啟用 AddressSanitizer 時(shí)的內(nèi)存泄漏怎么解決?
HarmonyOS5云服務(wù)技術(shù)分享--ArkTS開(kāi)發(fā)函數(shù)
HarmonyOS5云服務(wù)技術(shù)分享--ArkTS開(kāi)發(fā)Node環(huán)境
HarmonyOS5云服務(wù)技術(shù)分享--云函數(shù)創(chuàng)建配置指南
keithley 2600系列l(wèi)abiew vi中配置測(cè)量功能中的node in 和node out具體功能是什么?
KaihongOS操作系統(tǒng):開(kāi)發(fā)環(huán)境搭建
在樹(shù)莓派上構(gòu)建和部署 Node.js 項(xiàng)目
僅僅使用代碼,就能點(diǎn)亮樹(shù)莓派的 GPIO 世界
【干貨】什么是Node-RED?一文帶你了解!
使用OpenVINO?進(jìn)行推理時(shí)的內(nèi)存泄漏怎么解決?
內(nèi)存泄漏檢測(cè)工具Sanitizer介紹
Bun 1.2震撼發(fā)布:全力挑戰(zhàn)Node.js生態(tài)的JavaScript運(yùn)行時(shí)新星

Node.js 內(nèi)存泄漏問(wèn)題初探
評(píng)論