热点面试题:Node.js 中的垃圾回收机制?
前言
极度投入,深度沉浸,边界清晰
前端小菜鸡一枚,分享的文章纯属个人见解,若有不正确或可待讨论点可随意评论,与各位同学一起学习~欢迎关注
『前端进阶圈』
公众号 ,一起探索学习前端技术…
公号回复加群
或扫码
, 即可加入前端交流学习群,长期交流学习…
公号回复加好友
,即可添加为好友
文章分为最后总结和步骤讲解,可自需查看。废话不多数,先上总结。
- 总结:
- Node.js 所运行的内存属于 Chrome V8 所管理的内存,在 Javascirpt 中所创建的对象都是保存在堆内存中,V8 的垃圾回收机制按照对象的存活时间将内存主要分为:新生代和老生代。
- 新生代:简单来说,就是存活时间较短的对象所存储的地方。
- 主要采用的是 semispace,它将内存分为了两个空间:
From space
和To space
, 例如我们声明一个新对象,这个新对象会被放入From space
中,当From space
快满的时候,会遍历所有的对象,将活跃对象从From space
copy 到To space
中。在这个过程中,如果一个对象被 copy 了很多次,就会被认为是存活时间较长的对象,将会被放入老生代中。
- 主要采用的是 semispace,它将内存分为了两个空间:
- 老生代:相反,就是存活时间较长的对象所存储的地方。
- 主要采用的是
Mark-Sweep (标记清除)算法
,它会从执行栈和全局对象上找所有能访问到的对象,将他们标记为活跃对象,标记完成之后,进入清除阶段,将没有标记的对象清除(这个过程也就是将清除了对象的内存标记为空闲状态),最后,将空闲状态的内存进行释放。
- 主要采用的是
- 新生代:简单来说,就是存活时间较短的对象所存储的地方。
- Node.js 所运行的内存属于 Chrome V8 所管理的内存,在 Javascirpt 中所创建的对象都是保存在堆内存中,V8 的垃圾回收机制按照对象的存活时间将内存主要分为:新生代和老生代。
-
内存机制
- 整体来讲,Node 的内存分为两部分:
Chrome V8管理的部分(Javascript使用的部分)
,系统底层管理的部分(C++/C使用的部分)
-
Chrome V8 的内存管理机制
- Node 程序运行所占用的所有内存称为
常驻内存
,常驻内存
由以下几部分组成:- 代码区: 存放即将执行的代码片段
- 栈:存放局部变量
- 堆:存放对象和闭包上下文,V8 使用的垃圾回收机制管理堆内存
- 堆外内存:不通过 V8 分配,也不受 V8 管理。是 Buffer 对象的数据存储的地方
- A:除堆外内存,其余部分均由 V8 管理
- 栈的分配与回收非常直接,当程序离开某作用域后,其栈指针下移(也就是回退),整个作用域的局部变量都会出栈,内存将被收回
- 最复杂的部分是堆的管理,V8 使用垃圾回收机制进行堆的内存管理,也是开发中可能造成内存泄漏的部分,是开发者的关注点
- Node 程序运行所占用的所有内存称为
-
内存C/C++的部分
- 这是 Node 的原生部分,也是从根本上区别与前端 js 的部分,包括核心运行库,在一些核心模块的加载过程中,Node 会调用一个名为 js2c 的工具。这个工具会将核心的 js 模块代码以C数组的方式存储在内存中,以此来提升运行效率。
- 在这个部分,我们也不会有内存的使用限制,但是作为 C/C++ 扩展来使用大量内存的过程中,风险也是显而易见的。
- C/C++ 没有内存回收机制。作为没有 C/C++ 功底的纯前端程序员,不建议去使用这部分,因为 C/C++ 模块非常强大,如果对于对象生命周期的理解不够到位,而在使用大内存对象的情境中,很容易就造成内存溢出,导致整个 Node 的崩溃甚至是系统的崩溃。安全的使用大内存的方法就是使用 Buffer 对象。
-
- 整体来讲,Node 的内存分为两部分:
-
Node 中的 js 引擎也是 chrome 的 V8 引擎,所以垃圾回收机制也属于 V8 中的内部垃圾回收机制。
-
js 中的对象都是保存在堆内存中,在创建进程时,会分配一个默认的堆内存,当对象越来越大时,堆内存会动态的扩大,如果达到最大限制,堆内存就会溢出抛出错误,然后终止 node.js 进程。
-
V8 的垃圾回收机制根据对象的存活时间采用了不同的算法,内存主要分为新生代和老生代。
-
新生代(存活时间较短的对象):
- 新生代内存采取的是将内存分为两个空间(每一部分空间称为 semispace)
- From space: 新生命的对象会存放在此
- To space: 当做搬移的空间
- 新声明的对象会被放入 From space,From space 的对象紧密排布,通过指针,上一个对象紧贴着下一个对象,所以内存是连续的,我们不用担心内存碎片问题。
- Q:什么是内存碎片?
- 内存碎片分为:内部碎片和外部碎片两类
- 内部碎片:系统为某个功能分配了一定的内存,但是该功能最终的实现没有用完系统分配的内存,剩余的部分内存就被称为内存碎片中的内部碎片。
- 外部碎片:有一些连续性内存太小,无法被系统分配到某个功能所导致的浪费。
- 内部碎片:系统为某个功能分配了一定的内存,但是该功能最终的实现没有用完系统分配的内存,剩余的部分内存就被称为内存碎片中的内部碎片。
- 内存碎片分为:内部碎片和外部碎片两类
- Q:什么是内存碎片?
- 当 From space 快满了,就会遍历出活跃对象,将他们从 From space 复制到 To space, 此时,From space 就空了,然后会将 From 与 To 互换身份。如果一个对象被 copy 了很多次,就会被认为是存活时间较长的,将会被移入老生代中。
- A:这种基于 copy 的算法,优点是可以很好地处理内存碎片的问题,缺点是会浪费一些空间作为搬移的空间位置,此外因为拷贝比较耗费时间,所以不适合分配太大的内存空间,更多是做一种辅助垃圾回收。
- 将存活的对象从一个区复制(Scavenge 算法:是一种基于 copy 的算法)到另一个区,对原来的区进行内存释放,反复如此。当一个对象经过多次复制依然存活时,这个对象就会被移入老生代当中。
- Scavenge 算法(具体实现采用Cheney算法)原理:在垃圾回收的过程中,就是通过将存活对象在两个 semispace 空间之间进行复制。
- 优点:时间短。
- 缺点:只能使用一半堆内存。新生代对象生命周期短,适合此算法。
- 新生代内存采取的是将内存分为两个空间(每一部分空间称为 semispace)
-
老生代(存活时间较长的对象):
-
老生代的空间就比新生代要大得多了,放的是一些存活时间长的对象,用的是 Mark-Sweep (标记清除)算法。
-
标记清除的过程:
- 从根集 Root Set(执行栈和全局对象)往上找到所有能访问到的对象,给它们标记为活跃对象。
- 标记完后,就是清除阶段,将没有标记的对象清除,其实就是标记一下这个内存地址为空闲。
- 这种做法会导致 空闲内存空间碎片化,当我们创建了一个大的连续对象,就会找不到地方放下。这时候,就要用 Mark-Compact(标记整理)来将碎片的活跃对象做一个整合。
- Mark-Compact 会将所有活跃对象拷贝移动到一端,然后边界的另一边就是一整块的连续可用内存了。
- 考虑到 Mark-Sweep 和 Mark-Compact 花费的时间很长,且会阻塞 JavaScript 的线程,所以通常我们不会一次性做完,而是用 增量标记 (Incremental Marking)的方式。也就是做断断续续地标记,小步走的策略,垃圾回收和应用逻辑交替进行。
- 另外,V8 还做了并行标记和并行清理,以此来提高执行效率。
-
S: 老生代采取的是标记清除算法,遍历所有对象并标记仍然存活的对象,然后再清除阶段将没有标记的对象进行清除,最后将清除后的空间进行释放。
老生代 新生代(默认) 新生代(最大) 64位系统 1400MB 32MB 64MB 32位系统 700MB 16MB 32MB
-
-
注:垃圾回收是影响性能的因素之一,要尽量减少垃圾回收,尤其是全堆垃圾回收
对象存活时间 内存空间 老生代 存活时间较长或常驻内存的对象 –max-old-space-size命令设置老生代内存空间的最大值 新生代 存活时间较短的对象 –max-new-space-size命令设置新生代内存空间的大小
-
-
Q:V8 引擎为什么要将内存分为新老生代呢?
- R:垃圾回收机制(GC)有很多种,但没有一种能胜任所有场景,在实际应用中,需要根据对象的生存周期的长短时间使用不同的算法,以此来达到最好的效果。在 V8 中,按对象的存活时间将内存的垃圾回收机制进行不同的分代,然后分别对不同的内存使用不同的高效算法。所以有了新老生代之分。
-
Q: V8 为什么要限制堆内存的大小?
- R:因为 V8 垃圾回收机制的限制。垃圾回收会引起 js 线程暂停执行;内存太大,垃圾回收时间太长,在这个考虑下,直接限制了堆内存的大小。
-
Q: 如何让内存不受限制?
- R: 在 Node 中,使用 Buffer 可以读取超过V8内存限制的大文件。原因是Buffer对象不同于其他对象,它不经过 V8 的内存分配机制。这在于 Node 并不同于浏览器的应用场景。在浏览器中,JavaScript 直接处理字符串即可满足绝大多数的业务需求,而 Node 则需要处理网络流和文件I/O流,操作字符串远远不能满足传输的性能需求。
- R: 在不需要进行字符串操作时,可以不借助v8,使用 Buffer 操作,这样就不会受到v8的内存限制
-
Q:如何查看内存信息?
- 可以通过
process.memoryUsage()
方法拿到内存相关信息process.memoryUsage(); // output: { rss: 35454976, heapTotal: 7127040, heapUsed: 5287088, external: 958852, arrayBuffers: 11314 } /** * unit(单位):byte(字节) rss:常驻内存大小(resident set size),包括代码片段、堆内存、栈等部分。 heapTotal:V8 的堆内存总大小; heapUsed:占用的堆内存; external:V8 之外的的内存大小,指的是 C++ 对象占用的内存,比如 Buffer 数据。 arrayBuffers:ArrayBuffer 和 SharedArrayBuffer 相关的内存大小,属于 external 的一部分 */
- 可以通过
-
Q: 如何测试最大内存限制?
- 写一个脚本,用一个定时器,让一个数组不停地变大,并打印堆内存使用情况,直到内存溢出, 抛出错误
const format = function (bytes) { return (bytes / 1024 / 1024).toFixed(2) + "MB"; }; const printMemoryUsage = function () { const memoryUsage = process.memoryUsage(); console.log( `heapTotal: ${format(memoryUsage.heapTotal)}, heapUsed: ${forma( memoryUsage.heapUsed )}` ); }; const bigArray = []; setInterval(function () { bigArray.push(new Array(20 * 1024 * 1024)); printMemoryUsage(); }, 500);
- A: 不要用 Buffer 做测试。因为 Buffer 是 Node.js 特有的处理二进制的对象,它不是在 V8 中的实现的,是 Node.js 用 C++ 另外实现的,不通过 V8 分配内存,属于堆外内存
- 测试说明:使用电脑是 macbook M1 Pro,Node.js 版本为 v16.17.0,使用的 V8 版本是 9.4.146.26-node.22(通过 process.versions.v8 得到)
// result: heapTotal: 164.81 MB, heapUsed: 163.93 MB heapTotal: 325.83 MB, heapUsed: 323.79 MB heapTotal: 488.59 MB, heapUsed: 483.84 MB ... heapTotal: 4036.44 MB, heapUsed: 4003.37 MB heapTotal: 4196.45 MB, heapUsed: 4163.29 MB <--- Last few GCs ---> [28033:0x140008000] 17968 ms: Mark-sweep 4003.2 (4036.4) -> 4003.1 (4036.4) MB, 2233.8 / 0.0 ms (average mu = 0.565, current mu = 0.310) allocation failure scavenge might not succeed [28033:0x140008000] 19815 ms: Mark-sweep 4163.3 (4196.5) -> 4163.1 (4196.5) MB, 1780.3 / 0.0 ms (average mu = 0.413, current mu = 0.036) allocation failure scavenge might not succeed <--- JS stacktrace ---> FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory ...
- 可以看到,是在 4000 MB 之后超出了内存上限,发生堆溢出,然后退出了进程。说明在我的机器上,默认的最大内存为 4G。
- 实际最大内存和它运行所在的机器有关,如果你的机器的内存大小为 2G,最大内存将设置为 1.5G。
- 写一个脚本,用一个定时器,让一个数组不停地变大,并打印堆内存使用情况,直到内存溢出, 抛出错误
-
Q: Javascript 的部分是由 ChromeV8 接管的吗?那为什么仍然可以使用大量内存创建缓存呢?
- R: 是的,Chrome 和 Node 都采用 ChromeV8 作为 JS 的引擎,但是实际上他们所面对的对象是不同的,Node 面对的是数据提供,逻辑和I/O,而 Chrome 面对的是界面的渲染,数据的呈现。因此在 Chrome 上,几乎不会遇到大内存的情况,作为为 Chrome 的而生的 V8 引擎自然也不会考虑这种情况,因此才会出现内存限制。而现在,Node 面对这样的情况是不可以接受的,所以 Buffer 对象,是一个特殊的对象,它由更低层的模块创建,存储在 V8 引擎以外的内存空间上。
- R: 在内存的层面上讲 Buffer 和 V8 是平级的。
-
Q: 如何高效使用内存?
- 手动销毁变量
- js中能形成作用域的有函数调用、with和全局作用域
- 例如,在函数调用时,会创建对应的作用域,在执行结束后销毁,并且在该作用域申明的局部变量也会被销毁
- 标识符查找(即变量名) 先查找当前作用域,再向上级作用域,一直到全局作用域
- 变量主动释放 全局变量要直到进程退出才释放,导致引用对象常驻老生代,可以用delete删除或者赋undefined、null(delete删除对象的属性可能干扰v8,所以赋值更好)
- 慎用闭包
- 闭包是外部作用域访问内部作用域的方法,得益于高阶函数特性
var foo = function() { var bar = function() { var local = "内部变量"; return function() { return local; }; }; var baz = bar(); console.log(baz()); }; // 从上面代码知bar()返回一个匿名函数,一旦 有变量引用它,它的作用域将不会释放,直到没有引用。 // 注:把闭包赋值给一个不可控的对象时,会导致内存泄漏。使用完,将变量赋其他值或置空
- 大内存使用
- 使用stream,当我们需要操作大文件,应该利用Node提供的stream以及其管道方法,防止一次性读入过多数据,占用堆空间,增大堆内存压力。
- 使用Buffer,Buffer是操作二进制数据的对象,不论是字符串还是图片,底层都是二进制数据,因此Buffer可以适用于任何类型的文件操作。
Buffer对象本身属于普通对象,保存在堆,由V8管理,但是其储存的数据,则是保存在堆外内存,是有C++申请分配的,因此不受V8管理,也不需要被V8垃圾回收,一定程度上节省了V8资源,也不必在意堆内存限制。
- 手动销毁变量
-
Q: 内存泄露?
- 原因:缓存,队列消耗不及时,作用域未释放等
- 缓存:
- 限制内存当缓存,要限制好大小,做好释放
- 进程之间不能共享内存,所以用内存做缓存也是
- 为了加速模块引入,模块会在编译后缓存,由于通过exports导出(闭包),作用域不会释放,常驻老生代。要注意内存泄漏。
var arr = []; exports.hello = function() { arr.push("hello" + Math.random()); }; //局部变量arr不停增加内存占用,且不会释放,如果必须如此设计,要提供释放接口
- 队列状态
- 在生产者和消费者中间
- 监控队列的长度,超过长度就拒绝
- 任意的异步调用应该包含超时机制
- 内存泄露排查的工具
- node-heapdump
- 安装 npm install heapdump
- 引入 var heapdump = require(‘heapdump’);
- 发送命令kill -USR2 ,heapdump会抓拍一份堆内存快照,文件为heapdump-…heapsnapshot格式,是json文件
- node-memwatch
var memwatch = require('memwatch'); memwatch.on('leak', function(info) { console.log('leak:'); console.log(info); }); memwatch.on('stats', function(stats) { console.log('stats:') console.log(stats); });
- 在进程使用node-memwatch后,每次全堆垃圾回收,会触发stats事件,该事件会传递内存的统计信息
stats: { num_full_gc: 4, // 第几次全堆垃圾回收 num_inc_gc: 23, // 第几次增量垃圾回收 heap_compactions: 4, // 第几次对老生代整理 usage_trend: 0, // 使用趋势 estimated_base: 7152944, // 预估基数 current_base: 7152944, // 当前基数 min: 6720776, // 最小 max: 7152944 //最大 }
- node-heapdump
- 如果经过连续的5次垃圾回收后,内存仍没有被释放,意味有内存泄漏,node-memwatch会触发leak事件。
文章特殊字符描述:
- 问题标注
Q:(question)
- 答案标注
R:(result)
- 注意事项标准:
A:(attention matters)
- 详情描述标注:
D:(detail info)
- 总结标注:
S:(summary)
共同学习,写下你的评论
评论加载中...
作者其他优质文章