作者:五月君 链接:https://www.imooc.com/article/288799 来源:首发慕课网

本文已获作者 "五月君" 授权转载,原文首发于 "慕课网",可以点击原文查看。

对于 Node.js 服务端研发的同学来说,关于垃圾回收、内存释放这块不需要向 C/C++ 的同学那样在创建一个对象之后还需要手动创建一个 delete/free 这样的一个操作进行 GC(垃圾回收), Node.js 与 Java 一样,由虚拟机进行内存自动管理。

但是这样并不表示就此可以高枕无忧了,在开发中可能由于疏忽或者程序错误导致的内存泄漏也是一个很严重的问题,所以做为一名合格的服务端研发工程师,还是有必要的去了解下虚拟机是怎样使用内存的,遇到问题才能从容应对。

快速导航

  • Nodejs中的GC

  • Nodejs垃圾回收内存管理实践

    • 内存泄漏识别

    • 内存泄漏例子

    • 手动执行垃圾回收内存释放

  • V8垃圾回收机制

    • V8堆内存限制

    • 新生代与老生代

    • 新生代空间 & Scavenge 算法

    • 老生代空间 & Mark-Sweep Mark-Compact 算法

    • V8垃圾回收总结

  • 内存泄漏

    • 全局变量

    • 闭包

    • 慎将内存做为缓存

    • 模块私有变量内存永驻

    • 事件重复监听

    • 其它注意事项

  • 内存检测工具

Nodejs中的GC

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,这是来自 Node.js 官网的一段话,所以 V8 就是 Node.js 中使用的虚拟机,在之后讲解的 Node.js 中的 GC 其实就是在讲 V8 的 GC。

Node.js 与 V8 的关系也好比 Java 之于 JVM 的关系,另外 Node.js 之父 Ryan Dahl 在选择 V8 做为 Node.js 的虚拟机时 V8 的性能在当时已经领先了其它所有的 JavaScript 虚拟机,至今仍然是性能最好的,因此我们在做 Node.js 优化时,只要版本升级性能也会伴随着被提升。

Nodejs垃圾回收内存管理实践

先通过一个 Demo 来看看在 Node.js 中进行垃圾回收的过程是怎样的?

内存泄漏识别

在 Node.js 环境里提供了 process.memoryUsage 方法用来查看当前进程内存使用情况,单位为字节

  • rss(resident set size):RAM 中保存的进程占用的内存部分,包括代码本身、栈、堆。

  • heapTotal:堆中总共申请到的内存量。

  • heapUsed:堆中目前用到的内存量,判断内存泄漏我们主要以这个字段为准。

  • external:V8 引擎内部的 C++ 对象占用的内存。


/*** 单位为字节格式为 MB 输出*/
const format = function (bytes) {return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};/*** 封装 print 方法输出内存占用信息*/
const print = function() {const memoryUsage = process.memoryUsage();console.log(JSON.stringify({rss: format(memoryUsage.rss),heapTotal: format(memoryUsage.heapTotal),heapUsed: format(memoryUsage.heapUsed),external: format(memoryUsage.external),}));
}

内存泄漏例子

堆用来存放对象引用类型,例如字符串、对象。在以下代码中创建一个 Fruit 存放于堆中。


// example.js
function Quantity(num) {if (num) {return new Array(num * 1024 * 1024);}return num;
}function Fruit(name, quantity) {this.name = namethis.quantity = new Quantity(quantity)
}let apple = new Fruit('apple');
print();
let banana = new Fruit('banana', 20);
print();

执行以上代码,内存向下面所展示的,apple 对象 heapUsed 的使用仅有 4.21 MB,而 banana 我们对它的 quantity 属性创建了一个很大的数组空间导致 heapUsed 飙升到 164.24 MB。


$ node example.js{"rss":"19.94 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"}
{"rss":"180.04 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"}

我们在来看下内存的使用情况,根节点对每个对象都持有引用,则无法释放任何内容导致无法 GC,正如下图所展示的

手动执行垃圾回收内存释放

假设 banana 对象我们不在使用了,对它重新赋予一些新的值,例如 banana = null,看下此刻会发生什么?

结果如上图所示,无法从根对象在到达到 Banana 对象,那么在下一个垃圾回收器运行时 Banana 将会被释放。

让我们模拟一下垃圾回收,看下实际情况是什么样的?


// example.js
let apple = new Fruit('apple');
print();
let banana = new Fruit('banana', 20);
print();
banana = null;
global.gc();
print();

以下代码中 --expose-gc 参数表示允许手动执行垃圾回收机制,将 banana 对象赋为 null 后进行 GC,在第三个 print 打印出的结果可以看到 heapUsed 的使用已经从 164.24 MB 降到了 3.97 MB


$ node --expose-gc example.js
{"rss":"19.95 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"}
{"rss":"180.05 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"}
{"rss":"52.48 MB","heapTotal":"9.33 MB","heapUsed":"3.97 MB","external":"0.01 MB"}

下图所示,右侧的 banana 节点没有了任何内容,经过 GC 之后所占用的内存已经被释放了。

V8垃圾回收机制

垃圾回收是指回收那些在应用程序中不在引用的对象,当一个对象无法从根节点访问这个对象就会做为垃圾回收的候选对象。这里的根对象可以为全局对象、局部变量,无法从根节点访问指的也就是不会在被任何其它活动对象所引用。

V8堆内存限制

内存在服务端本来就是一个寸土寸金的东西,在 V8 中限制 64 位的机器大约 1.4GB,32 位机器大约为 0.7GB。因此,对于一些大内存的操作需谨慎否则超出 V8 内存限制将会造成进程退出。

一个内存溢出超出边界限制的例子

 

// overflow.js
const format = function (bytes) {return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};const print = function() {const memoryUsage = process.memoryUsage();console.log(`heapTotal: ${format(memoryUsage.heapTotal)}, heapUsed: ${format(memoryUsage.heapUsed)}`);
}const total = [];
setInterval(function() {total.push(new Array(20 * 1024 * 1024)); // 大内存占用print();
}, 1000)

以上例子中 total 为全局变量每次大约增长 160 MB 左右且不会被回收,在接近 V8 边界时无法在分配内存导致进程内存溢出。


$ node overflow.js
heapTotal: 166.84 MB, heapUsed: 164.23 MB
heapTotal: 326.85 MB, heapUsed: 324.26 MB
heapTotal: 487.36 MB, heapUsed: 484.27 MB
heapTotal: 649.38 MB, heapUsed: 643.98 MB
heapTotal: 809.39 MB, heapUsed: 803.98 MB
heapTotal: 969.40 MB, heapUsed: 963.98 MB
heapTotal: 1129.41 MB, heapUsed: 1123.96 MB
heapTotal: 1289.42 MB, heapUsed: 1283.96 MB<--- Last few GCs --->[87581:0x103800000]    11257 ms: Mark-sweep 1283.9 (1290.9) -> 1283.9 (1290.9) MB, 512.1 / 0.0 ms  allocation failure GC in old space requested
[87581:0x103800000]    11768 ms: Mark-sweep 1283.9 (1290.9) -> 1283.9 (1287.9) MB, 510.7 / 0.0 ms  last resort GC in old space requested
[87581:0x103800000]    12263 ms: Mark-sweep 1283.9 (1287.9) -> 1283.9 (1287.9) MB, 495.3 / 0.0 ms  last resort GC in old space requested<--- JS stacktrace --->

在 V8 中也提供了两个参数仅在启动阶段调整内存限制大小

分别为调整老生代、新生代空间,关于老生代、新生代稍后会做介绍。

  • --max-old-space-size=2048

  • --max-new-space-size=2048

当然内存也并非越大越好,一方面服务器资源是昂贵的,另一方面据说 V8 以 1.5GB 的堆内存进行一次小的垃圾回收大约需要 50 毫秒以上时间,这将会导致 JavaScript 线程暂停,这也是最主要的一方面。

新生代与老生代

绝对大多数的应用程序对象的存活周期都会很短,而少数对象的存活周期将会很长为了利用这种情况,V8 将堆分为两类新生代和老生代,新空间中的对象都非常小大约为 1-8MB,这里的垃圾回收也很快。新生代空间中垃圾回收过程中幸存下来的对象会被提升到老生代空间。

新生代空间

由于新空间中的垃圾回收很频繁,因此它的处理方式必须非常的快,采用的 Scavenge 算法,该算法由 C.J. Cheney 在 1970 年在论文 A nonrecursive list compacting algorithm 提出。

Scavenge 是一种复制算法,新生代空间会被一分为二划分成两个相等大小的 from-space 和 to-space。它的工作方式是将 from space 中存活的对象复制出来,然后移动它们到 to space 中或者被提升到老生代空间中,对于 from space 中没有存活的对象将会被释放。完成这些复制后在将 from space 和 to space 进行互换。

Scavenge 算法非常快适合少量内存的垃圾回收,但是它有很大的空间开销,对于新生代少量内存是可以接受的。

老生代空间

新生代空间在垃圾回收满足一定条件(是否经历过 Scavenge 回收、to space 的内存占比)会被晋升到老生代空间中,在老生代空间中的对象都已经至少经历过一次或者多次的回收所以它们的存活概率会更大。在使用 Scavenge 算法则会有两大缺点一是将会重复的复制存活对象使得效率低下,二是对于空间资源的浪费,所以在老生代空间中采用了 Mark-Sweep(标记清除) 和 Mark-Compact(标记整理) 算法。

Mark-Sweep

Mark-Sweep 处理时分为标记、清除两个步骤,与 Scavenge 算法只复制活对象相反的是在老生代空间中由于活对象占多数 Mark-Sweep 在标记阶段遍历堆中的所有对象仅标记活对象把未标记的死对象清除,这时一次标记清除就已经完成了。

看似一切 perfect 但是还遗留一个问题,被清除的对象遍布于各内存地址,产生很多内存碎片。

Mark-Compact

在老生代空间中为了解决 Mark-Sweep 算法的内存碎片问题,引入了 Mark-Compact(标记整理算法),其在工作过程中将活着的对象往一端移动,这时内存空间是紧凑的,移动完成之后,直接清理边界之外的内存。

V8垃圾回收总结

为何垃圾回收是昂贵的?V8 使用了不同的垃圾回收算法 Scavenge、Mark-Sweep、Mark-Compact。这三种垃圾回收算法都避免不了在进行垃圾回收时需要将应用程序暂停,待垃圾回收完成之后在恢复应用逻辑,对于新生代空间来说由于很快所以影响不大,但是对于老生代空间由于存活对象较多,停顿还是会造成影响的,因此,V8 又新增加了增量标记的方式减少停顿时间。

关于 V8 垃圾回收这块笔者讲的很浅只是自己在学习过程中做的总结,如果你想了解更多原理,深入浅出 Node.js 这本书是一个不错的选择,还可参考这两篇文章 A tour of V8: Garbage Collection、 Memory Management Reference.。

内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

全局变量

未声明的变量或挂在全局 global 下的变量不会自动回收,将会常驻内存直到进程退出才会被释放,除非通过 delete 或 重新赋值为 undefined/null 解决之间的引用关系,才会被回收。关于全局变量上面举的几个例子中也有说明。

闭包

这个也是一个常见的内存泄漏情况,闭包会引用父级函数中的变量,如果闭包得不到释放,闭包引用的父级变量也不会释放从而导致内存泄漏。

一个真实的案例 — The Meteor Case-Study,2013年,Meteor 的创建者宣布了他们遇到的内存泄漏的调查结果。有问题的代码段如下


var theThing = null
var replaceThing = function () {var originalThing = theThingvar unused = function () {if (originalThing)console.log("hi")}theThing = {longStr: new Array(1000000).join('*'),someMethod: function () {console.log(someMessage)}};
};
setInterval(replaceThing, 1000)

以上代码运行时每次执行 replaceThing 方法都会生成一个新的对象,但是之前的对象没有释放导致的内存泄漏。这块涉及到一个闭包的概念 “同一个作用域生成的闭包对象是被该作用域中所有下一级作用域共同持有的” 因为定义的 unused 使用了作用域的 originalThing 变量,因此 replaceThing 这一级的函数作用域中的闭包(someMethod)对象也持有了 originalThing 变量( 重点:someMethod的闭包作用域和unused的作用域是共享的),之间的引用关系就是 theThing引用了longStr和someMethodsomeMethod引用了originalThingoriginalThing又引用了上次的theThing,因此形成了链式引用。

上述代码来自 Meteor blog An interesting kind of JavaScript memory leak,更多理解还可参考 Node-Interview issues #7 讨论

慎将内存做为缓存

通过内存来做缓存这可能是我们想到的最快的实现方式,另外业务中缓存还是很常用的,但是了解了 Node.js 中的内存模型和垃圾回收机制之后在使用的时候就要谨慎了,为什么呢?缓存中存储的键越多,长期存活的对象也就越多,垃圾回收时将会对这些对对象做无用功。

以下举一个获取用户 Token 的例子,memoryStore 对象会随着用户数的增加而持续增长,以下代码还有一个问题,当你启动多个进程或部署在多台机器会造成每个进程都会保存一份,显然是资源的浪费,最好是通过 Redis 做共享。


const memoryStore = new Map();exports.getUserToken = function (key) {const token = memoryStore.get(key);if (token && Date.now() - token.now > 2 * 60) {return token;}const dbToken = db.get(key);memoryStore.set(key, {now: Date.now(),val: dbToken,});return token;
}

模块私有变量内存永驻

在加载一个模块代码之前,Node.js 会使用一个如下的函数封装器将其封装,保证了顶层的变量(var、const、let)在模块范围内,而不是全局对象。

这个时候就会形成一个闭包,在 require 时会被加载一次,将 exports 对象保存于内存中,直到进程退出才会回收,这个将会导致的是内存常驻,所以对一个模块的引用建议仅在头部引用一次缓存起来,而不是在使用时每次都加载,否则也会造成内存增加。


(function(exports, require, module, __filename, __dirname) {// 模块的代码实际上在这里
});

事件重复监听

在 Node.js 中对一个事件重复监听则会报如下错误,实际上使用的 EventEmitter 类,该类包含一个 listeners 数组,默认为 10 个监听器超出这个数则会报警如下所示,用于发现内存泄漏,也可以通过 emitter.setMaxListeners() 方法为指定的 EventEmitter 实例修改限制。

(node:23992) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit

Cnode 论栏有篇文章分析了 Socket 重连导致的内存泄漏,参考 原生Socket重连策略不恰当导致的泄漏,还有 Node.js HTTP 模块 Keep-Alive 产生的内存泄漏,参考 Github Node Issues #714

其它注意事项

在使用定时器 setInterval 时记的使用对应的 clearInterval 进行清除,因为 setInterval 执行完之后会返回一个值且不会自动释放。另外还有 map、filter 等对数组进行操作,每次操作之后都会创建一个新的数组,将会占用内存,如果单纯的遍历例如 map 可以使用 forEach 代替,这些都是开发中的一些细节,但是往往细节决定成败,每一次的内存泄漏也都是一次次的不经意间造成的。因此,这些点也是需要我们注意的。


console.log(setInterval(function(){}, 1000)) // 返回一个 id 值
[1, 2, 3].filter(item => item % 2 === 0) // [2]
[1, 2, 3].map(item => item % 2 === 0) // [false, true, false]

内存检测工具

node-heapdump

heapdump是一个dumpV8堆信息的工具,node-heapdump

node-profiler

node-profiler 是 alinode 团队出品的一个 与node-heapdump 类似的抓取内存堆快照的工具,node-profiler

Easy-Monitor

轻量级的 Node.js 项目内核性能监控 + 分析工具,https://github.com/hyj1991/easy-monitor

Node.js-Troubleshooting-Guide

Node.js 应用线上/线下故障、压测问题和性能调优指南手册,Node.js-Troubleshooting-Guide

alinode

Node.js 性能平台(Node.js Performance Platform)是面向中大型 Node.js 应用提供 性能监控、安全提醒、故障排查、性能优化等服务的整体性解决方案。alinode

Node.js 内存管理和 V8 垃圾回收机制相关推荐

  1. JS高级-自执行函数-垃圾回收机制及内存管理

    自执行函数 函数分为两种: (1)一般函数 预解析后通过函数调用 函数名( ) 执行 (2)自执行函数 js引擎一遇到整个函数就立马执行 代码如下: js中的垃圾回收机制及内存管理 内存管理 (1)全 ...

  2. 深入理解谷歌最强V8垃圾回收机制

    有很多人都听说过V8引擎,但可能不是很了解,V8名称叫Chrome V8,是由谷歌开源的一个高性能 JavaScript 引擎.该引擎采用 C++ 编写,Google Chrome 浏览器用的就是这个 ...

  3. jvm对象从新生代到老年代_JVM内存管理、JVM垃圾回收机制、新生代、老年代以及永久代...

    内存模型 JVM运行时数据区由程序计数器.堆.虚拟机栈.本地方法栈.方法区部分组成,结构图如下所示. JVM内存结构由程序计数器.堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)程序计数器 ...

  4. 【大厂面试】堆的内存结构及GC垃圾回收机制

    前言 本文主要介绍堆内存的结构及对象在堆中的生命周期和垃圾回收. 目录 前言 一.堆的结构 1.1新生区 1.2 养老区 1.3堆结构的代码验证

  5. 有意思的 Node.js 内存泄漏问题

    作者:elvinpeng,腾讯 WXG 前端开发工程师 Node.js 使用的是 V8 引擎,会自动进行垃圾回收(Garbage Collection,GC),因而写代码的时候不需要像 C/C++ 一 ...

  6. 设置log缓存_全局变量、事件绑定、缓存爆炸?Node.js内存泄漏问题分析

    作者:elvinpeng,腾讯 WXG 前端开发工程师 Node.js 使用的是 V8 引擎,会自动进行垃圾回收(Garbage Collection,GC),因而写代码的时候不需要像 C/C++ 一 ...

  7. Node的垃圾回收机制与内存溢出捕获(上)

    Node的垃圾回收机制与内存溢出捕获 一.什么是Node的内存?   想必大家在用JavaScript开发的过程中,不太关心内存的管理,因为对于前端来说,浏览器的内存几乎不会出现用完的情况,因为所接触 ...

  8. JS 内存泄漏与垃圾回收机制

    前言 不管什么程序语言,内存生命周期基本是一致的: 分配你所需要的内存 使用分配到的内存(读.写) 不需要时将其释放\归还 所有语言 第二部分都是明确的,第一和第三部分在 底层语言 中是明确的.但在像 ...

  9. 浏览器(V8)的垃圾回收机制

    文章目录 内存管理 什么是浏览器中的垃圾? 常见的GC算法 一. 引用计数 二. 标记清除 三. 标记整理 V8内核 V8的垃圾回收策略 V8内存分配 新生代区域及算法 老生代区域及算法 内存管理 在 ...

最新文章

  1. mysql语句大全 新浪博客_MySQL语句入门
  2. Windows环境下利用VS和mingw编译LLVM
  3. Python之np.where的使用
  4. Helm 3 完整教程(十六):Helm 函数讲解(10)版本语义化函数、URL函数、UUID函数
  5. Gym - 100625G Getting Through 计算几何+并查集
  6. covariance matrix r语言_R语言 第2章 数据对象与数据读写(3)
  7. 算法导论的道与术、工程师思维奠定能走多远
  8. 都2022年了,这11个Java开发工具你还不知道?
  9. 《对比Excel,轻松学习Python数据分析》读书笔记------Pandas入门
  10. 软件文档的概念和细分
  11. 【Git命令】git commit --amend
  12. 华为天才少年——稚晖君!
  13. 恶意软件同源性方法研究
  14. 军团的崛起:利用多态指挥多兵种作战
  15. 网络安全认证与加密协议算法整合
  16. 数据库读写分离与分库分表
  17. spring boot自动化配置
  18. 【SpringBoot整合缓存】-----spring-boot-starter-cache篇
  19. 织梦DedeCMS采集图片路径怎么按月存放
  20. php创建网址打不开,php网站无法打开怎么办

热门文章

  1. 刚刚,腾讯每人发100股:市值7万,不算年终奖!员工:愿为小马哥拼命
  2. 信息安全研究方向和内容
  3. 乐视视频唤不回“生机”
  4. simplify逆向_Simplify-SDK-一种以代码形式管理基础架构的框架。
  5. background-position为什么会出现负值?
  6. 奇迹mu 架设过程中可能会出现的问题及解决办法
  7. [计算机网络] 子网划分(详解)一个自治系统有5个局域网,其连接图如图所示。LAN2至LAN5上的主机数分别为91,150,3和15。该自治系统分配到的IP地址块为30.138.118/23。试...
  8. 【mysql】哦买噶!多么强大的group by语句
  9. LeetCode.M33.搜索旋转排序数组
  10. 【Python】selenium自动化打卡