作者 | 后端Team朱捷峰
整理 | 包包
V8 垃圾回收机制
事实上,我们平时在写 Node.js 的时候很少去关心内存问题,那是因为 Node.js 对 Google V8 进行封装,底层的垃圾收回机制都交给 V8 处理。大部分时候,是不会有内存问题的。相对于 C/C++ 这类需要自己管理内存的语言,Node.js 有更加平滑的学习曲线,这也是 Node.js 最大的优势之一。但是也总有意外情况,可能导致 Node.js 进程内存泄漏。
那么如何避免我们的 Node.js 程序出现内存泄漏的情况呢?我们先来了解下 V8 内存管理机制。
一个进程通常是通过在内存中分配空间来体现的,这个空间我们称之为 Resident Set(常驻空间)。V8 将内存分为了以下几块:
• 代码区:实际正在运行的代码
• 栈区:包含了所有的值类型(数字、布尔值等)、指向存储在堆区的对象指针、定义程序控制流的指针
• 堆区:专门用来存储引用类型的内存区域,比如对象、字符串和闭包
在 Node.js 中,我们可以通过调用process.memoryUsage() 方法来来查询内存使用情况。该函数返回值如下:
memory usage
{
rss: 4935680,
heapTotal: 1826816,
heapUsed: 650472,
external: 49879
}
以上数值以字节为单位
• rss:表示 Resident Set 的大小
• heapTotal:表示堆的总大小
• heapUsed:表示堆的实际使用大小
• external:表示 V8 管理的绑定到 JavaScript 对象的 C++ 对象的大小
我们知道在 Node.js 的运行时中,JavaScript 是由 V8 编译成可执行的机器码。运行时的数据结构是由 V8 来管理的,我们能做的很有限。通过 JavaScript 我们是没法做到分配内存和释放内存的。
V8 的垃圾回收算法实现还是很复杂的,感兴趣的同学可以参考:http://newhtml.net/v8-garbage-collection/。但是我们仍然可以把原理简单抽象:如果一个内存片段没有被任何地方引用,我们可以假设它不再会被用到,那么该内存片段可以被释放。
上图表示在内存中各个对象的引用情况,只有当红球对象不再被任何对象引用的时候,它才能被回收。
异常情况
既然 V8 会进行垃圾回收,那我们为什么还要关心内存情况呢?
理想情况,内存占用会保持在一个相对稳定的范围:
实际上,我们仍然可能会看到内存占用升高的情况:
V8 垃圾回收机制尽可能地回收和释放内存,但是每次执行垃圾回收以后,内存占用仍然持续上升,这明显就是内存泄漏了。
制造内存泄漏
有一些很明显的情况会导致内存泄漏:1、比如将每位访客的 IP 记录在 global 上存储数组上;2、再比如著名的“ 沃尔玛内存泄漏事件”,它是由 Node.js 核心代码中一个遗漏的声明引发的血案,工程师们花了好几个星期去排查并最终得以解决。
在这篇文章里,我们就不一一列举所有可能产生问题的错误情况。我们来看一下一个难以排查的情况,代码很简单,你可以自己运行调试:
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}!`));
初看的话,这段代码没啥问题。我们可以想象 theThing 在每次调用 replaceThing() 时会被重写。问题就在于,someMethod 有闭包作用域作为上下文,这就意味着当调用 someMethod 时,unused 是可见的。虽然实际上 unused 并没有被调用,但是它却阻止了 V8 垃圾回收机制对 originalThing 进行回收。这就是我们平时所说的“循环引用”:
既然找到问题所在,那么如何解决呢?答案很简单,我们只要切断循环引用就可以了,这里我们只需要在 replaceThing 这个方法最后加入 theThing = null。
针对这个问题,我们还可以通过 ESLint 的 no-unused-vars 规则来避免定义了但是未使用的变量,这样可以减少循环引用的可能性。
排查问题
理解了垃圾回收的原理,那么我们平常在码代码的时候也要注意避免循环引用的情况出现。但是就像上面这种情况,有时候就是防不胜防。那么遇到问题的时候,我们应该如何排查呢?
推荐一下我写的一个小工具 heapsnapshot.js ,可以获取生成堆的快照信息,如下图:
然后利用 Chrome 开发者工具,Memory 来做具体分析:
请选择相邻的3个堆快照文件,导入 Memory 分析工具中,如下图:
第一步,先选择 Profiles 中的第二个文件,然后筛选 Objects 选项选择“Objects allocated between 1539255057342 and 1539255076968 ”,然后在 Constructor 中进行具体的分析 。
第二步,同理对第二个和第三个文件进行对比分析。找到两次分析都出现过的元素,重点排查,定位到具体的问题代码,再做修改。
第三步,重复上述过程,检查内存泄漏问题是否解决。
以上只是对 Node.js 内存问题的一个初步探讨,感兴趣的话推荐大家去看下 V8 垃圾回收的原理。平常我们在编码的时候也要注意尽量避免产生循环引用,但是如果遇到了也不要担心,可以通过上面的步骤排查解决。
-
内存
+关注
关注
8文章
2962浏览量
73796 -
NODE.JS
+关注
关注
1文章
46浏览量
32743
发布评论请先 登录
相关推荐
评论