背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效。

已读完书籍《架构简洁之道》。

当前阅读周书籍《深入浅出的Node.js》

内存控制

V8的垃圾回收机制与内存限制

对于性能敏感的服务器端程序,内存管理的好坏、垃圾回收状况是否优良,都会对服务构成影响。而在Node中,这一切都与Node的JavaScript执行引擎V8息息相关。

Node与V8

Node在JavaScript的执行上直接受益于V8,可以随着V8的升级就能享受到更好的性能或新的语言特性(如ES5和ES6)等,同时也受到V8的一些限制。

V8的内存限制

在Node中通过JavaScript使用内存时就会发现只能使用部分内存,在这样的限制下,将会导致Node无法直接操作大内存对象。

在Node中使用的JavaScript对象基本上都是通过V8自己的方式来进行分配和管理的。V8的内存管理机制在Node中,限制了开发者随心所欲使用大内存的想法。

V8的对象分配

在V8中,所有的JavaScript对象都是通过堆来进行分配的。Node提供了V8中内存使用量的查看方式。

V8的垃圾回收机制

V8的垃圾回收策略主要基于分代式垃圾回收机制。

现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存施以更高效的算法。

图1:3种垃圾回收算法的简单对比

回收算法

Mark-Sweep

Mark-Compact

Scavenge

速度

中等

最慢

最快

空间开销

少(有碎片)

少(有碎片)

双倍空间(无碎片)

是否移动对象

高效使用内存

在V8面前,开发者所要具备的责任是如何让垃圾回收机制更高效地工作。

在正常的JavaScript执行中,无法立即回收的内存有闭包和全局变量引用这两种情况。由于V8的内存限制,要十分小心此类变量是否无限制地增加,因为它会导致老生代中的对象增多。

内存指标

一些没有被回收的对象,会导致内存占用无限增长。一旦增长达到V8的内存限制,将会得到内存溢出错误,进而导致进程退出。

查看内存使用情况

process.memoryUsage()以及os模块中的totalmem()和freemem()方法可以查看内存使用情况。

1、查看进程的内存占用

调用process.memoryUsage()可以看到Node进程的内存占用情况:

$ node
> process.memoryUsage()
{
  rss: 24449024,
  heapTotal: 6049792,
  heapUsed: 4099320,
  external: 1743637,
  arrayBuffers: 75467
}

2、查看系统的内存占用

os模块中的totalmem()和freemem()这两个方法用于查看操作系统的内存使用情况,它们分别返回系统的总内存和闲置内存,以字节为单位。

$ node
> os.totalmem()
17179869184
> os.freemem()
918704128
>

堆外内存

堆外内存指的是那些不是通过V8分配的内存。

堆外内存一般不会有堆内存的大小限制。所以利用堆外内存可以突破内存限制的问题。

内存泄漏

Node对内存泄漏十分敏感,一旦线上应用有成千上万的流量,那怕是一个字节的内存泄漏也会造成堆积,垃圾回收过程中将会耗费更多时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用崩溃。

在V8的垃圾回收机制下,在通常的代码编写中,很少会出现内存泄漏的情况。但是内存泄漏通常产生于无意间,较难排查。

通常,造成内存泄漏的原因有如下几个:

  1. 缓存。
  2. 队列消费不及时。
  3. 作用域未释放。

所以针对不同的泄露原因可以制定适合的预防方案:

  1. 慎将内存当做缓存。目前比较好的解决方案是采用进程外的缓存,进程自身不存储状态。
  2. 关注队列状态。使用队列也会不经意产生的内存泄漏。遇到类似场景,表层的解决方案是换用消费速度更高的技术。深度的解决方案应该是监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员。另一个解决方案是任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限值。

内存泄漏排查

现在已经有许多工具用于定位Node应用的内存泄漏,下面是一些常见的工具:

  • v8-profiler:由Danny Coates提供,它可以用于对V8堆内存抓取快照和对CPU进行分析,但该项目已经有3年没有维护了。
  • node-heapdump:这是Node核心贡献者之一Ben Noordhuis编写的模块,它允许对V8堆内存抓取快照,用于事后分析。
  • node-mtrace:由Jimb Esser提供,它使用了GCC的mtrace工具来分析堆的使用。❑ dtrace。在Joyent的SmartOS系统上,有完善的dtrace工具用来分析内存泄漏。
  • node-memwatch:来自Mozilla的Lloyd Hilaiel贡献的模块,采用WTFPL许可发布。

大内存应用

Node提供了stream模块用于处理大文件。stream模块是Node的原生模块,直接引用即可。

如下代码为读取一个文件,然后将数据写入到另一个文件的过程:

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.on('data', function (chunk) {
  writer.write(chunk);
});
reader.on('end', function () {
  writer.end();
});

注:如果不需要进行字符串层面的操作,则不需要借助V8来处理,可以尝试进行纯粹的Buffer操作,这不会受到V8堆内存的限制。

总结

我们来总结一下本篇的主要内容:

  • 内存在Node中不能随心所欲地使用,但也不是完全不擅长。在Node中,这一切都与Node的JavaScript执行引擎V8息息相关。
  • Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。
  • Node对内存泄漏十分敏感,一旦内存泄漏造成堆积,垃圾回收过程中将会耗费更多时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用崩溃。所以要针对内存泄露的情况,采取适当的预防方案。
  • Node提供了stream模块用于处理大文件。stream模块是Node的原生模块,直接引用即可。

作者介绍非职业「传道授业解惑」的开发者叶一一。《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。