
Node.js使用V8引擎并将自动执行垃圾回收(Garbage Collection,GC),因此在编写代码时,您无需像C / C ++那样手动分配和释放内存空间,这更加方便,但仍然需要注意内存的使用,以免引起内存泄漏(内存泄漏).
内存泄漏通常非常隐蔽. 例如,您可以在以下代码中看到问题所在吗?
let theThing = null;
let replaceThing = function() {
const newThing = theThing;
const unused = function() {
if (newThing) console.log("hi");
};
// 不断修改引用
theThing = {
longStr: new Array(1e8).join("*"),
someMethod: function() {
console.log("a");
},
};
// 每次输出的值会越来越大
console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);
如果可以的话,欢迎加入我们的微信支付海外团队,共同追求卓越. 如果您现在看不到它,让我们一起阅读本文.
本文的前半部分将首先介绍一些理论知识,然后给出定位内存泄漏的示例. 有兴趣的朋友可以直接看一下这个例子.
总体结构
从上图可以看到,Node.js的常驻集分为两部分: 堆和栈,具体来说:
堆栈: 用于存储原始数据类型,此处还记录了函数调用的堆栈和弹出.
堆栈的空间由操作系统管理,开发人员不需要太在意;堆的空间由V8引擎管理. 可能由于代码问题而导致内存泄漏,或者在很长一段时间后,垃圾回收会导致程序运行缓慢.
我们可以通过以下代码简单地观察Node.js的内存使用情况:
const format = function (bytes) {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
};
const memoryUsage = process.memoryUsage();
console.log(JSON.stringify({
rss: format(memoryUsage.rss), // 常驻内存
heapTotal: format(memoryUsage.heapTotal), // 总的堆空间
heapUsed: format(memoryUsage.heapUsed), // 已使用的堆空间
external: format(memoryUsage.external), // C++ 对象相关的空间
}, null, 2));
外部是与C ++对象相关的空间. 例如,当通过新的ArrayBuffer(100000);申请一块缓冲存储器时,您可以清楚地看到外部空间的增加.

相关空间的默认大小可以通过以下参数(以MB为单位)进行调整:
更常用的是--max_new_space_size和--max-old-space-size.
有很多与新一代Scavenge回收算法和旧版Mark-Sweep和Mark-Compact算法有关的文章,因此在此不再赘述. 例如,本文讨论了Node.js内存管理和V8垃圾回收机制.
内存泄漏
由于代码不正确,有时不可避免地会发生内存泄漏. 有四种常见方案:
全局变量关闭参考事件绑定缓存爆炸
让我们通过示例进行讨论.
全局变量
未使用var / let / const声明的变量将直接绑定到Global对象(Node.js)或Windows对象(在浏览器中). 即使不再使用它们,也不会自动回收它们: <
function test() {
x = new Array(100000);
}
test();
console.log(x);
此代码的输出为[],您可以看到在完成测试功能后,尚未释放数组x.
关闭参考
关闭引起的内存泄漏通常非常隐蔽. 例如,您可以在以下代码中看到问题所在吗?

let theThing = null;
let replaceThing = function() {
const newThing = theThing;
const unused = function() {
if (newThing) console.log("hi");
};
// 不断修改引用
theThing = {
longStr: new Array(1e8).join("*"),
someMethod: function() {
console.log("a");
},
};
// 每次输出的值会越来越大
console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);
运行此代码,您可以看到输出使用的堆内存越来越大,关键是在当前V8实现中,闭包对象由当前范围内的所有内部函数范围共享,这意味着Thing.someMethod和unUsed共享相同的关闭上下文,这导致Thing.someMethod隐式持有对先前newThing的引用,因此Thing-> someMethod-> newThing-> last theThing-> ...导致执行of longStr: 每次执行replaceThing函数时都会使用new Array(1e8).join(“ *”),并且不会自动回收它,从而导致占用更多的内存. 越大,内存最终泄漏.
上述问题有一个非常聪明的解决方案: 通过引入新的块级范围,newThing的声明和使用与外部隔离,从而破坏了共享并防止了循环引用.
let theThing = null;
let replaceThing = function() {
{
const newThing = theThing;
const unused = function() {
if (newThing) console.log("hi");
};
}
// 不断修改引用
theThing = {
longStr: new Array(1e8).join("*"),
someMethod: function() {
console.log("a");
},
};
console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);
这里,{...}形成了单独的块级作用域,并且没有外部引用,因此newThing将在GC期间自动回收. 例如,在我的计算机上运行此代码时,输出如下:
2097128
2450104
2454240
...
2661080
2665200
2086736 // 此时进行垃圾回收释放了内存
2093240
事件绑定
事件绑定导致的内存泄漏在浏览器中非常常见. 它们通常是由于未及时删除事件响应函数,导致重复绑定或删除DOM元素后未处理事件响应函数这一事实引起的,例如以下React代码:
class Test extends React.Component {
componentDidMount() {
window.addEventListener('resize', function() {
// 相关操作
});
}
render() {
return test component;
}
}
该组件在安装时侦听了resize事件,但是在删除该组件时未处理相应的功能. 如果安装和拆卸非常频繁,则许多无用的事件监视功能将绑定到窗口,最终导致内存泄漏. 可以通过以下方式避免此问题:
class Test extends React.Component {
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
handleResize() { ... }
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
render() {
return <div>test componentdiv>;
}
}
缓存爆炸
Object / Map的内存缓存可以大大提高程序的性能,但是很有可能缓存的大小和过期时间没有得到很好的控制,并且无效数据仍被缓存在内存中,导致在内存泄漏中:

const cache = {};
function setCache() {
cache[Date.now()] = new Array(1000);
}
setInterval(setCache, 100);
在上面的代码中,缓存是连续设置的,但是没有代码可以释放缓存,最终导致内存突发.
如果确实需要内存缓存,则强烈建议使用npm软件包lru-cache,它可以设置缓存有效期和最大缓存空间,并通过LRU消除算法避免缓存爆炸.
内存泄漏位置的实际操作
当发生内存泄漏时,通常很难定位,主要有两个原因:
程序开始运行时,问题不会立即暴露出来,并且需要一段时间的连续运行,甚至一两天,问题才会再次出现. 该错误消息非常模糊,通常只能看到堆内存不足错误消息.
在这种情况下,可以使用两种工具来确定问题: Chrome DevTools和heapdump. heapdump的功能就像它的名字一样-生成并导出内存中堆的状态信息,然后将其导入Chrome DevTools中以查看特定的详细信息,例如堆中有哪些对象以及有多少空间它占有更多.
接下来,让我们使用上面的闭包参考中的内存泄漏示例进行实际操作. 首先,npm install heapdump安装后,将代码修改为如下所示:
// 一段存在内存泄漏问题的示例代码
const heapdump = require('heapdump');
heapdump.writeSnapshot('init.heapsnapshot'); // 记录初始内存的堆快照
let i = 0; // 记录调用次数
let theThing = null;
let replaceThing = function() {
const newThing = theThing;
let unused = function() {
if (newThing) console.log("hi");
};
// 不断修改引用
theThing = {
longStr: new Array(1e8).join("*"),
someMethod: function() {
console.log("a");
},
};
if (++i >= 1000) {
heapdump.writeSnapshot('leak.heapsnapshot'); // 记录运行一段时间后内存的堆快照
process.exit(0);
}
};
setInterval(replaceThing, 100);
在第3行和第22行中,将导出初始状态的快照和1000个周期后的快照,并将其另存为init.heapsnapshot和Leak.heapsnapshot.
然后打开Chrome浏览器,按F12调出DevTools面板,单击“内存”选项卡,最后使用“加载”按钮依次导入两个快照:
标记

导入后,您可以看到左侧的堆内存显着增加,从1.7 MB增加到3.1 MB,几乎翻了一番:
下一步是最关键的步骤. 单击泄漏快照并将其与init快照进行比较:
在右侧的红色框中圈出两列:
您会看到增长最快的前两个项目是串联的字符串和结束符,因此让我们单击以查看哪些是:
从这两张图可以直观地看出,这主要是由Thing.someMethod函数的关闭上下文和Thing.longStr的很长的拼接字符串引起的内存泄漏. 问题基本上在这里. 现在,我们还可以单击下面的“对象”模块以更清楚地查看调用链的关系:
从图中可以明显看出,内存泄漏的原因是由于newTHing
参考文章
正确可视化V8 EngineGithub内存泄漏示例中的内存管理ali节点打开的Chrome devtools
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-312049-1.html
有钱了
你你可以委婉的说
超期待