每日前端夜话第276篇

翻译:疯狂的技术宅

作者:Giovanny Gongora

来源:nodesource

正文共:3955 字

预计阅读时间:10分钟

一直以来,跟踪 Node.js 的内存泄漏是一个反复出现的话题,人们始终希望对其复杂性和原因了解更多。

并非所有的内存泄漏都显而易见。但是,一旦我们确定了其模式,就必须在内存使用率,内存中保存的对象和响应时间之间寻找关联。在检查对象时,应该根据自己所用的框架或技术(例如服务器端渲染),研究收集了多少对象,以及它们是否正常。希望在完成本文结束之后,你将能够理解并寻找一种策略来调试 Node.js 程序的内存消耗。

Node.js 中的垃圾回收机制

JavaScript 是一种垃圾回收语言,而 Google 的 V8 最初是为 Google Chrome 创建的JavaScript引擎,在许多情况下都可以用作独立的运行时。Node.js 中垃圾收集器的两个重要操作是:

  1. 确定有用的或无用的对象,并且

  2. 回收或重用无用对象所占用的内存。

需要记住的要点:在垃圾回收器运行时,它将完全暂停你的程序,直到完成工作为止。因此,你需要通过维护对象的引用来最大程度地减少其工作。

V8 JavaScript 引擎会自动分配和取消分配 Node.js 进程使用的所有内存。让我们看看实际情况是怎样的。

如果你将内存视为一个树结构,那么可以想象 V8 从“根节点”开始保存程序中所有的变量。这可能是你的 window 对象,也可能是 Node.js 模块中的全局对象,通常称为控制者。需要牢记的一点是,你无法对怎样取消分配“根”节点进行控制。

接下来,你将找到一个 Object 节点,通常被称为叶子(没有子引用的节点)。最后 JavaScript 中有 4 种数据类型:布尔值,字符串,数字和对象。

V8 将遍历该树并尝试识别无法从“根”节点访问的数据组。如果无法从“根”节点访问该数据,则 V8 假定不再使用该数据,并释放内存。请记住:要确定某个对象是否处于活动状态,需要检查是否可通过被定义为活动对象的某个指针链到达;其他所有的情况,例如无法从根节点访问,或无法被根节点或另一个活动对象引用的对象,都会被视为垃圾。

简而言之,垃圾收集器有两个主要任务:

  1. 跟踪

  2. 计算对象之间的引用。

当你需要跟踪来自另一个进程的远程引用时,它可能会变得很棘手,但是在 Node.js 程序中,我们通常用单进程,这样使我们更加轻松。

V8 的内存方案

V8 使用类似于 Java 虚拟机的方案,并将内存划分为多个段。实现这种包装方案的东西被称为“驻留集”,它是指在 RAM 中驻留的进程所占用的内存部分。

在驻留集中,你会发现:

  • 代码段:代码实际执行的位置。

  • 栈: 包含局部变量和所有值类型,其指针引用堆上的对象或定义程序的控制流。

  • 堆: 专门用于存储引用类型(如对象、字符串和闭包)的内存段。

还有重要的两点要记住:

  • 对象的浅大小:保存对象本身所需的内存大小

  • 对象的保留大小:当删除对象及其依赖对象时,被释放的内存大小

Node.js 有一个对象,以字节为单位描述 Node.js 进程的内存使用情况。在对象内部,你会发现:

  • rss: 是指驻留集大小。

  • heapTotal 和 heapUsed: 是指 V8 的内存使用情况。

  • external: 是指与 V8 所管理的 JavaScript 对象绑定的 C++ 对象的内存使用情况。

查找泄漏

Chrome DevTools 是一个很棒的工具,可用于通过远程调试来诊断 Node.js 程序中的内存泄漏。也有其他为你提供类似功能的工具。但是,你需要记住,概要分析是一项繁重的 CPU 任务,可能会对你的程序产生负面影响,一定要注意这一点!

我们将要介绍的 Node.js 程序是一个简单的 HTTP API Server,它具有多个端点,向使用该服务的人返回不同的信息。你可以克隆这个程序的repository。

 1const http = require('http') 2 3const leak = [] 4 5function requestListener(req, res) { 6 7  if (req.url === '/now') { 8    let resp = JSON.stringify({ now: new Date() }) 9    leak.push(JSON.parse(resp))10    res.writeHead(200, { 'Content-Type': 'application/json' })11    res.write(resp)12    res.end()13  } else if (req.url === '/getSushi') {14    function importantMath() {15      let endTime = Date.now() + (5 * 1000);16      while (Date.now() 17        Math.random();18      }19    }2021    function theSushiTable() {22      return new Promise(resolve => {23        resolve('?');24      });25    }2627    async function getSushi() {28      let sushi = await theSushiTable();29      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })30      res.write(`Enjoy! ${sushi}`);31      res.end()32    }3334    getSushi()35    importantMath()36  } else {37    res.end('Invalid request')38  }39}4041const server = http.createServer(requestListener)42server.listen(process.env.PORT || 3000)

启动Node.js应用程序:

我们一直在使用 3S(3 Snapshot)方法进行诊断并确定可能的内存问题。有趣的是,我们发现这是 Gmail 团队的 Loreena Lee 长期使用的一种解决内存问题的方法。此方法的步骤:

  1. 打开 Chrome DevTools 并访问 chrome://inspect

  2. 在底部的“Remote Target”中,单击 inspect 按钮。

注意: 要确保已将 Inspector 附加到要分析的 Node.js 程序。你还可以用 ndb 连接到 Chrome DevTools。

当应用运行时,你将在控制台的输出中看到一条 Debugger Connected 消息。

  1. 转到 Chrome DevTools > Memory

  2. 获取堆快照

在这种情况下,我们得到了第一个快照,而服务没有进行任何负载或处理。这是针对某些用例的提示:如果我们能够确定在接受请求或进行某些处理之前不需要对程序进行任何预热,那就很好了。有时,在获取第一个堆快照之前先进行热身操作是有意义的,因为在某些情况下,你可能会在第一次调用时对全局变量进行了延迟初始化。

  1. 在你的程序中执行你认为导致内存泄漏的操作。

在这种情况下,我们将运行  npm run load-mem。这将启动 ab 来模拟 Node.js 应用程序中的流量或负载。

  1. 得到堆快照

  1. 再次在你的程序中执行你认为会导致内存泄漏的操作。

  2. 获取最终的堆快照

  1. 选择最新得到的快照。

  2. 在窗口顶部,找到显示 “All objects” 的下拉列表,并将其切换为“Objects allocated between snapshots 1 and 2”。(如果需要,你也可以对 2 和 3 执行相同的操作)。这将大大减少你看到的对象数量。

比较视图也可以帮你识别那些对象:

在该视图中,你将看到泄漏对象的列表:顶级条目(每个构造函数一行)、对象到GC根的距离、对象实例数、浅大小和保留大小。你可以通过选择一行来查看其内容。一个好的经验法则是,首先忽略括号中的项目,因为它们是内置结构。@ 字符是对象的唯一 ID,可让你比较每个对象的堆快照。

典型的内存泄漏可能是通过意外地将对对象的引用存储在无法进行垃圾回收的全局对象中,从而保留了预期仅在一个请求周期内持续存在的对象的引用。

这个例子故意留下了一个内存泄漏的问题,在请求一个从 API 查询返回的对象时生成带有日期时间戳的随机对象,并将其存储在全局数组中来泄漏该对象。通过查看几个保留的对象,你会看到一些泄漏数据的示例,可用于跟踪应用程序中的泄漏。

NSolid 非常适合这种类型的用例,因为它可以使你很好地了解在执行的每个任务或负载测试中内存是怎样增加的。如果你感到好奇,还可以实时查看每个性能分析动作如何影响 CPU。

demo

在实际项目中,你不可能总是盯着用于监视程序的工具。NSolid 的一大优点是可以为应用程序的不同指标设置阈值和限制。例如,你可以将 NSolid 设置为在使用的内存量超过 X 时,或者在 X 时间内尚未从高消耗高峰恢复内存的情况下,进行堆快照。听起来不错吧?

标记和清理

V8 的垃圾收集器主要基于 Mark-Sweep 收集算法,该算法包括跟踪垃圾收集,该操作通过标记可达的对象,然后清理内存并回收未标记的对象(必须无法访问),将其纳入释放列表。这也称为世代垃圾收集器,对象可以在新声代、从新生代到老生代、以及老生代中移动。

移动对象的代价非常打,因为需要将对象的基础内存复制到新位置,并且指向这些对象的指针也需要更新。

用人话解释:

V8 递归查找所有对象到“根”节点的引用路径。例如:在 JavaScript 中,“window” 对象是可以充当 Root 的全局变量的示例。window 对象始终存在,因此垃圾收集器可以认为它及其所有子对象始终存在(即不是垃圾)。如果有任何引用,则没有指向“根”节点的路径。特别是当它以递归方式查找未引用的对象时,将被标记为垃圾,稍后将会被清除以释放该内存并将其返回给操作系统。

但是,现代的垃圾收集器以不同的方式对这种算法进行了改进,但本质是相同的:可访问的内存被标记为一类,其余的被视为垃圾。

请记住,从根可以访问到的所有内容均不视为垃圾。不需要的引用是保留在代码中某个位置的变量,这些变量将不再使用,并且指向可以释放的内存,因此,要了解 JavaScript 中最常见的泄漏,我们需要了解通常忘记引用的方式。

Orinoco 垃圾收集器

Orinoco 是最新 GC 项目的代号,它利用最新的增量和并发技术进行垃圾回收,并有释放主线程的功能。描述 Orinoco 性能的重要指标之一是垃圾回收器执行时主线程暂停的频率和时间。对于经典的“世界末日”收集者而言,这些时间间隔会因为延迟、质量差的渲染以及响应时间的增加而影响程序的用户体验。

V8 在新声代内存中的辅助流之间分配垃圾回收工作(清除)。每个流接收一组指针,然后将所有活动对象移动到“to-space”

将对象移至“to-space”时,线程需要通过读、写、比较和交换的原子操作进行同步,以避免出现另一个线程找到相同的对象但遵循不同路径并尝试移动的情况。

引用自 V8 官网:

在现有 GC 中添加并行、增量和并发技术是一项多年的努力,但已取得了回报,将大量工作移交给了后台任务。它大大改善了暂停时间、延迟和页面加载,使动画、滚动和用户交互更加顺畅。并行的 Scavenger 根据工作量将主线程新声代垃圾收集的总时间减少了大约 20%–50%。Idle-time GC 可以在 Gmail 空闲时将其 JavaScript 堆内存减少 45%。并发标记和清除可以将笨重的 WebGL 游戏中的暂停时间减少多达 50%。

Mark-Evacuate 收集器包括三个阶段:标记、复制和更新指针。为了避免在新声代中清理页面以维护空闲列表,仍然使用 semi-space 来维护新生代,它始终保持紧凑状态,即在垃圾回收期间将活动对象复制到 “to-space” 中。并行进行的好处是可以获得“exact liveness”信息。通过仅移动和重新链接主要包含活动对象的页面,可以用此信息来避免复制,这也可以由完整的 Mark-Sweep-Compact 收集器执行。它通过和标记清除算法相同的方式标记堆中的活动对象来工作,这意味着堆通常会被碎片化。V8 当前随附有并行的 Scavenger,可在大量基准测试中减少主线程新生代垃圾回收约 20%–50% 的总时间

与暂停主线程、响应时间和页面加载有关的所有方面都得到了显着改善,这使得页面上的动画、滚动和用户交互更加流畅。并行收集器可以将新内存的总处理时间减少 20–50%,具体取决于负载。但是工作还没有结束:减少停顿仍然是一项重要任务,我们将继续寻找使用更先进的技术来实现这一目标的可能性。

总结

大多数开发人员在开发 JavaScript 程序时无需考虑 GC,但是了解一些内部知识可以帮助你考虑内存使用情况和有用的编程模式。例如考虑到 V8 中基于世代的堆结构,从 GC 角度来说,维护低生存期的对象的成本实际上是相当低的,因为我们主要为存在的对象付出代价。这种模式不仅特定于 JavaScript,而且对于许多支持垃圾回收的语言也都有效。

要点:

  • 请勿使用过时或不推荐的软件包(例如,node-memwatch,node-inspector 或 v8-profiler)来检查内存。你需要的一切都已经集成在了 Node.js 的二进制文件中(尤其是 node.js 检查器和调试器)。如果你需要更专业的工具,则可以使用 NSolid、Chrome DevTools 或其他知名软件。

  • 考虑在何时何地触发堆快照和 CPU profile。由于要在生产环境中进行快照,你将会希望同时触发这两者(主要是在测试中),所以这会需要大量的 CPU 操作。另外,在关闭进程和进行冷重启之前,请确认有多少堆转储被写入了。

  • 没有哪一种工具可以解决所有问题。要根据程序的具体情况进行测试、测量、判断和解决。选择适合你体系结构的最佳工具,并选择一种可以提供更多有用数据来帮你解决问题的工具。

原文:https://nodesource.com/blog/memory-leaks-demystified

 

2020年京程一灯全新课程体系即将推出,请保持关注。

愿你在新的一年里保持技术领先,有个好前程,愿你月薪30K。我们是认真的 !

✎往期精彩回顾

面向开发人员的十大 NodeJS 框架

JavaScript 类完整指南

讲给前端的正则表达式

WebAssembly 正式成为 Web 的第四种语言

2020 年 Node.js 将会有哪些新功能

2020 年 Web 开发展望

从 JavaScript、ES6、ES7 到 ES10,你学到哪儿了?

15个 Vue.js 高级面试题

js日期比较大小_node.js 内存泄漏的秘密相关推荐

  1. js 日期比较大小,js判断日期是否在区间内,js判断时间段是否在另外一个时间段内...

    /** * 日期解析,字符串转日期 * @param dateString 可以为2017-02-16,2017/02/16,2017.02.16 * @returns {Date} 返回对应的日期对 ...

  2. javascript内存泄漏调试工具mac_node.js 内存泄漏的秘密

    一直以来,跟踪 Node.js 的内存泄漏是一个反复出现的话题,人们始终希望对其复杂性和原因了解更多. 并非所有的内存泄漏都显而易见.但是,一旦我们确定了其模式,就必须在内存使用率,内存中保存的对象和 ...

  3. Node.js 应用故障排查手册 —— 雪崩型内存泄漏问题

    楔子 实践篇一中我们也看到了一个比较典型的由于开发者不当使用第三方库,而且在配置信息中携带了三方库本身使用不到的信息,导致了内存泄漏的案例,实际上类似这种相对缓慢的 Node.js 应用内存泄漏问题我 ...

  4. JS 程序中可能存在的内存泄漏

    目录 一.不正当的闭包 二.隐式全局变量 三.游离DOM引用 四.遗忘的定时器 五.遗忘的事件监听器 六.排查问题 一.不正当的闭包 闭包是指有权访问另一个函数作用域中的变量的函数,通常情况闭包就是函 ...

  5. js垃圾回收机制和引起内存泄漏的操作

    Js具有自动垃圾回收机制.垃圾收集器会按照固定的时间间隔周期性的执行. JS中最常见的垃圾回收方式是标记清除. 工作原理:是当变量进入环境时,将这个变量标记为"进入环境".当变量离 ...

  6. 面试题——js垃圾回收机制和引起内存泄漏的操作

    JS的垃圾回收机制了解吗? Js具有自动垃圾回收机制.垃圾收集器会按照固定的时间间隔周期性的执行. JS中最常见的垃圾回收方式是标记清除. 工作原理:是当变量进入环境时,将这个变量标记为"进 ...

  7. JS中4种常见的内存泄漏

    一.什么是内存泄漏 本质上讲,内存泄漏是当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或空闲内存池的现象. 二.几种常见的内存泄漏 1.意外的全局变量 一个未声明变量的引 ...

  8. node.js mysql 不退出_node.js,node-mysql_使用了node-mysql的代码无法自动停止,node.js,node-mysql - phpStudy...

    使用了node-mysql的代码无法自动停止 // db.js var mysql = require('mysql'); module.exports = function () { var poo ...

  9. js 多个定时器_Node.js系列深入浅出Node模块化开发——CommonJS规范

    前言 本文将为大家透彻的介绍关于Node的模块化--CommonJS的一切. 看完本文可以掌握,以下几个方面: 什么是模块化,以及没有模块化会带来哪些问题,是如何解决的: JavaScript的设计缺 ...

最新文章

  1. NHibernate和Cuyahoga(二)(翻译):
  2. 最近遇到的若干技术问题
  3. Netty源码分析第7章(编码器和写数据)----第2节: MessageToByteEncoder
  4. 含金量高的计算机大赛,高含金量计算机竞赛盘点!
  5. IIS32位,64位模式下切换
  6. sicktim571操作手册_TIM中文操作手册.PDF
  7. c语言表达式运算符,C语言运算符与表达式
  8. 企业管理的实质和核心是人的管理
  9. 《深入浅出WPF》笔记——资源篇
  10. aliddns ipv6_利用阿里云ddns动态解析ipv6地址
  11. 【script】python 调用阿里云解析 DNS API 实现 DDNS(动态域名解析)
  12. 前端web要学mysql吗_学前端必须学习HTML和js吗?
  13. 抓包PC微信小程序失败解决方法
  14. 最近删除的照片在哪里?已删除照片恢复
  15. Could NOT find Vulkan (missing: Vulkan_LIBRARY Vulkan_INCLUDE_DIR) | 解压安装
  16. [高数][高昆轮][高等数学上][第二章-导数与微分]04.隐函数及由参数方程所确定的函数的导数...
  17. VOC2007数据集 VOC2012数据集下载 百度云
  18. getchar()函数的使用方法
  19. OSGI框架的功能和设计思想
  20. 云渲染是什么?云渲染好处以及安装方法。

热门文章

  1. 开源.NET企业级应用系统 OpenVista
  2. thinkphp5 内置接口开发与使用
  3. 20155204 2016-2017-2 《Java程序设计》第8周学习总结
  4. 【ASP.NET Web API2】初识Web API
  5. 对于.swp文件的恢复方法
  6. 初学者应该了解的一些SQL语句及hr 用户解锁相关
  7. [推荐]ORACLE SQL:经典查询练手第三篇(不懂装懂,永世饭桶!)
  8. 在statspack显示完整的SQL
  9. 微信小程序——账号及开发工具
  10. springboot工程打包时将application.properties放在jar包外