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

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

V8的内存限制

在一般的后端开发语言中,在基本的内存使用上没有什么限制,然而在Node中通过JavaScript使用内存时就会发现只能使用部分内存(64位系统下约为1.4GB,32位系统下约为0.7GB),在这样的限制下,将会导致Node无法直接操作大内存对象,比如无法将一个2GB的文件读入内存中进行字符串分析处理。

造成这个问题的主要原因在于Node基于V8构建,所以在Node中使用的JavaScript对象基本都是通过V8自己的方式来进行分配和管理的。V8的这套内存管理机制在浏览器的应用场景下使用起来绰绰有余,但在Node中,却限制了开发者随心使用大内存的想法。

V8对象分配

在V8中所有的JavaScript对象都是通过堆来进行分配的,在Node中使用process.memoryUsage()能够得到内存的使用信息对象,其中heapTotal 和 heapUsed 代表 V8 的内存使用情况,已申请的和已使用的。 external 代表 V8 管理的,绑定到 Javascript 的 C++ 对象的内存使用情况。 rss 是驻留集大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分),这些物理内存中包含堆、代码段、以及栈。

在当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中,如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制为止。

V8为什么要限制堆的大小

表层原因是V8最初为浏览器而设计,不太可能遇到大量内存的使用场景。深层原因是V8的垃圾回收机制的限制,按官方的说法,以1.5GB的垃圾回收堆为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这里的时间指的是在垃圾回收中引起JavaScript线程暂停执行的时间。很明显,这是无法接受的,因此直接限制了堆内存的大小。

当然,这个限制也可以打开,Node在启动时可以传递--max-old-space-size或 --max-new-space-size来调整内存限制的大小

node --max-old-space-size=1700 test.js // 单位为MB 设置老生代内存空间的最大值

node --max-new-space-size=1024 test.js // 单位为KB 设置新生代内存空间的最大值

只能在初始化的时候设置,一旦生效就不能再动态改变。

V8的垃圾回收机制

在V8中,主要将内存分为新生代和老生代两代,新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

V8的整体大小就是新生代所用内存空间加上老生代的内存空间,在64位系统中,老生代内存为1400MB,新生代内存为32MB,在32位系统中,则分别为700MB、16MB。又因为新生代内存有2个,所以在64位系统中,V8堆内存的最大值为1464MB,而在32位系统中则为732MB。

新生代:Scavenge算法

新生代中的对象主要通过Scavenge算法进行垃圾回收,它将新生代内存一分为二,每一部分的空间成为semispace(半空间),在这两个semispace中,只有一个处于使用中,成为From空间;另一个处于闲置状态,称为To空间。当我们分配对象时,先是在From空间中进行分配,当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将被释放,完成赋值后,From空间和To空间的角色发生对换。

Scavenge是典型的牺牲空间换取时间的算法,但Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在事件效率上有优异的表现,而新生代中对象的生命周期较短,恰恰适合这个算法。

当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象,这种对象随后会被移动到老生代中,这个过程被称为晋升。

对象晋升的条件主要有两个,一个是是否经历过Scavenge回收,一个是To空间的内存占用比超过限制。

对象从From空间中复制到To空间时:

第一种情况:检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收,是就直接复制到老生代空间中,没有就复制到To空间。

第二种情况:判断To空间中是否已经使用超过了25%,如果超过了,则直接晋升到老生代空间中,否则就复制To空间。设置25%这个限制值的原因是当这次Scavenge回收完成后,这个To空间就将变成From空间,接下来的内存分配会在这个空间中进行,如果占比较高,会影响后续的内存分配。

老生代:Mark-Sweep & Mark-Compact

对于老生代存活对象占较大比重,那么继续采用Scavenge算法复制存活对象将效率很低,而且Scavenge算法会浪费一半的空间。为此V8在老生代主要采用了Mark-Sweep & Mark-Compact相结合的方式进行垃圾回收。

Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段,它会在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段阶段只清除没有被标记的对象,可以看出,Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象,活对象在新生代中只占较小部分,死对象在老生代中只占较小部分,这是在新老生代使用这两种不同回收方式的原因。

Mark-Sweep最大的问题在于进行一次标记清楚回收后,内存空间会出现不连续的状态,下图中黑色的部分就会死亡对象,清除之后就出现这种个问题,这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成这次分配,就会提前触发垃圾回收,而这次回收是不必要的。

mark-sweep.png

而Mark-Compact就是为了解决Mark-Sweep的内存碎片问题,是指标记整理的意思,是在Mark-Sweep的基础上演变而来的,他们的差别在于对象在标记死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理调边界外的内存。

mark-compact.png

在V8中这两种回收策略是结合使用的,但是由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,所以在取舍上,V8主要使用Mark-Sweep,在空间不足以对新生代中晋升过来的对象进行分配时才使用Mark-Compact。

{{% notice info %}}
为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被成为“全停顿”(stop-the-world)
{{% /notice %}}

在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大, 但老生代通常配置得较大,且存活对象较多,全堆垃圾回收的标记、清理、整理等动作造成的停顿就比较可怕,需要设法改善。

为了降低这种停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为了增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一次“步进”,就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。

V8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右,V8后续还移入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理动作也变成增量式的,同时还计划引入并行标记和并行清理,进一步利用多核性能降低每次停顿的时间。

{{% notice tip %}}
对于V8的垃圾回收特点和JavaScript在单线程上的执行情况,垃圾回收是影响性能的因素之一,想要高性能的执行效率,需要注意让垃圾回收尽量收的进行,尤其是全堆垃圾回收。
{{% /notice %}}

高效使用内存

作用域

函数在每次调用时会创建对应的作用域,函数执行结束后,该作用域将会销毁,同时作用域中声明的局部变量分配在该作用域上,随着作用域的销毁而销毁,只被局部变量引用的对象存活周期较短,在作用域释放之后,局部变量就会失效,其引用的对象将会在下次垃圾回收时内被释放。

JavaScript在执行时回去查找该变量定义在哪里,它最先查找的是当前作用域,如果在当前作用域中无法找到该变量的声明,将会向上级的作用域里查找,直到查到为止。

变量的主动释放

如果变量是全局变量,由于全局作用域需要直到进程退出才能释放,此时将导致引用的对象常驻在老生代中,如果需要释放常驻内存的对象,可以使用delete操作来删除引用关系,或者将变量重新赋值,让旧的对象脱离引用关系,在接下来的老生代内存清除和整理的过程中,会被回收释放。

global.foo = 'abc';
delete global.foo;

// 或者重新赋值
global.foo = undefined // or null;

{{% notice tip %}}
同样,如果在非全局作用域中,想要主动释放变量引用的对象,也可以通过这样的方式,虽然delete操作和重新赋值具有相同的效果,但是在V8中通过delete删除对象的属性有可能干扰V8的优化,所以通过赋值方式解除引用更好。
{{% /notice %}}

闭包

在JavaScript中,实现外部作用域访问内部作用域中的变量的方法叫做闭包(closure),这得益于高阶函数的特性,函数可以作为参数或者返回值。

虽说局部变量将会随着作用域的销毁而被回收,但是闭包返回的是一个匿名函数,这个函数中具备了访问局部变量的条件,虽然在后续的执行中,外部作用域还是无法直接返回局部变量,但是若要访问它,只要通过这个中间函数稍作周转即可。

闭包是JavaScript的高级特性,利用它可以产生很多巧妙的效果,它的问题在于,一旦有变量引用这个中间函数,这个中间函数就不会被释放,同时也会使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放,除非不再引用,才会逐步释放。在正常的JavaScript执行中,无法立即回收的内存有闭包和全局变量引用这两种情况,由于V8的内存限制,要十分小心此类变量是否无限制地增加,因为它会导致老生代中的对象增多。

内存指标

前面我们提到了process.memoryUsage()可以查看内存使用的情况,除此之外,os模块中的totalmem()和freemem()方法也可以查看内存使用情况,这两个方法用于查看操作系统的内存使用情况,它们分别返回系统的总内存和闲置内存。

$ node
> process.memoryUsage()
{ rss: 13852672,
heapTotal: 6131200,
heapUsed: 2757120 }

其中rss是resident set size的简写,即进程的常驻内存部分,进程的内存总共有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)中,这3个值的单位都是字节。


var showMem = function () {
  var mem = process.memoryUsage();
  var format = function (bytes) {
    return (bytes / 1024 / 1024).toFixed(2) + ' MB';
  };
  console.log('Process: heapTotal ' + format(mem.heapTotal) +
  ' heapUsed ' + format(mem.heapUsed) + ' rss ' + format(mem.rss));
  console.log('-----------------------------------------------------------');
};

var useMem = function () {
  var size = 20 * 1024 * 1024;
  var arr = new Array(size);
  for (var i = 0; i < size; i++) {
    arr[i] = 0;
  }
  return arr;
};
var total = [];
for (var j = 0; j < 15; j++) {
  showMem();
  total.push(useMem());
}
showMem();
$ node outofmemory.js
Process: heapTotal 3.86 MB heapUsed 2.10 MB rss 11.16 MB
----------------------------------------------------------------
Process: heapTotal 357.88 MB heapUsed 353.95 MB rss 365.44 MB
----------------------------------------------------------------
Process: heapTotal 520.88 MB heapUsed 513.94 MB rss 526.30 MB
----------------------------------------------------------------
Process: heapTotal 679.91 MB heapUsed 673.86 MB rss 686.14 MB
----------------------------------------------------------------
Process: heapTotal 839.93 MB heapUsed 833.86 MB rss 846.16 MB
----------------------------------------------------------------
Process: heapTotal 999.94 MB heapUsed 993.86 MB rss 1006.93 MB
----------------------------------------------------------------
Process: heapTotal 1159.96 MB heapUsed 1153.86 MB rss 1166.95 MB
----------------------------------------------------------------
Process: heapTotal 1367.99 MB heapUsed 1361.86 MB rss 1375.00 MB
----------------------------------------------------------------
FATAL ERROR: CALL_AND_RETRY_2 Allocation failed - process out of memory

可以看到,每次调用useMem到导致了3个值的增长,在接近1500MB的时候,无法继续分配内存,然后进程内存溢出了,连循环体都无法执行完成,仅执行了7次。

通过process.memoryUsage()的结果可以看到,堆中的内存用量总是小于进程的常驻内存用量,这意味着Node中的内存使用并非都是通过V8进行分配的,我们将那些不是通过V8分配的内存称为堆外内存。

在这里我们修改useMem方法,将Array改为Buffer,将size变大,每一次构造200MB的对象。

var useMem = function () {
  var size = 200 * 1024 * 1024;
  var buffer = new Buffer(size);
  for (var i = 0; i < size; i++) {
    buffer[i] = 0;
  }
  return buffer;
};

重新执行该代码:

$ node out_of_heap.js
Process: heapTotal 3.86 MB heapUsed 2.07 MB rss 11.12 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.94 MB rss 212.88 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.95 MB rss 412.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.95 MB rss 612.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.92 MB rss 812.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.92 MB rss 1012.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1212.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1412.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1612.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1812.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 2012.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 2212.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 2412.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 2612.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 2812.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 3012.91 MB
----------------------------------------------------------------

我们看到15次循环都完整执行,并且三个内存占用值与前一个示例完全不同,heapTotal和heapUsed变化极小,唯一变化的是rss的值,并且该值以及远远超过V8的限制值,这其中的原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制,所以也不会有堆内存的大小限制,这意味着使用堆外内存可以突破内存限制的问题。

为什么Bufer对象并非通过V8分配,这在于Node并不同于浏览器的应用场景,在浏览器中,JavaScript直接处理字符串即可满足绝大多数的业务需求,而Node则需要处理网络流和文件I/O流,操作字符串远远不能满足传输的性能需求,因此Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分,受V8的垃圾回收限制的主要是V8的堆内存。

内存泄漏

内存泄漏会造成堆积,垃圾回收过程中将会消耗更多时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用崩溃。尽管内存泄漏的情况不尽相同,但其实质只有一个,那就是应当回收的对象出现意外而没有被回收,变成了常驻的老生代中的对象。通常造成内存泄漏的原因有如下几个:缓存、队列消费不及时、作用域未释放。

慎将内存当做缓存

JavaScript开发者通常喜欢用对象的键值对来缓存东西,但这与严格意义上的缓存又有着区别,严格意义的缓存有着完善的过期策略,而普通的键值对并没有。

_.memoize = function(func, hasher) {
  var memo = {};
  hasher || (hasher = _.identity);
  return function() {
    // 根据函数的参数形成不同的hash值,以此为键将结果缓存在memo上。
    var key = hasher.apply(this, arguments);
    return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
  };
};

它的原理是以参数作为键进行缓存,以内存空间换CPU执行时间,这里潜藏的陷阱即是每个被执行的结果都会按参数缓存在memo对象上,不会被清楚。这在前端网页这种短时应用场景中不存在大问题,但是执行量大和参数多样性的情况,会造成内存占用不释放,如果需要,可以限制缓存对象的大小,加上过期策略以防止内存无限制增长。

var LimitableMap = function (limit) {
  this.limit = limit || 10;
  this.map = {};
  this.keys = [];
};
var hasOwnProperty = Object.prototype.hasOwnProperty;

LimitableMap.prototype.set = function (key, value) {
  var map = this.map;
  var keys = this.keys;
  if (!hasOwnProperty.call(map, key)) {
    // 如果没有缓存,判断对象拥有的属性数量是否超过限制
    if (keys.length === this.limit) {
      // 超过限制,淘汰第一次缓存的数据,再缓存
      var firstKey = keys.shift();
      delete map[firstKey];
    }
    // 未超过限制,缓存起来
    keys.push(key);
  }
  // 如果有缓存,直接替换
  map[key] = value;
};

LimitableMap.prototype.get = function (key) {
  return this.map[key];
};
module.exports = LimitableMap;

另一个案例在于模块机制,为了加速模块的引入,所有的模块都会通过编译执行,然后被缓存起来,由于通过exports导出的函数可以访问文件模块中的私有变量,这样每个文件模块在编译执行后形成的作用域因为模块缓存的原因不会被释放。

由于模块的缓存机制,模块是常驻老生代的,所有在设计模块时,要十分小心内存泄漏的情况。也可以添加清空队列的相应接口,以供调用者释放内存。

缓存的解决方案

直接将内存作为缓存的方案要十分慎重,除了限制缓存的大小,另外要考虑的事情是,进程之间无法共享内存,如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费,如何使用大量缓存,目前比较好的解决方案是采用进程外的缓存,进程自身不存储状态,外部缓存软件有着良好的缓存过期淘汰策略以及自有的内存管理,不影响Node进程的性能,它的好处多多,在Node中主要可以解决以下两个问题。

1、将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效。

2、进程之间可以共享缓存。

{{% notice info %}}
其中较好的缓存有RedisMemcached
{{% /notice %}}

关注队列状态

队列在消费者-生产者模型中经常充当中间产物,这是一个容易忽略的情况,因为大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生,但是一旦消费速度低于生产速度,将会形成堆积。

比如:有的应用会收集日志,如果欠缺考虑,也许会采用数据库来记录日志,日志通常是海量的,而数据库的写入效率远远低于文件直接写入,于是会造成数据库写入操作的堆积,而JavaScript相关的作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏。

表层的解决方案,是换用消费速度更高的技术,换用文件写入日志的方式,但是需要注意的是,如果生产速度因为某些原因突然激增,后者消费速度因为突然的系统故障降低,内存泄漏还是可能出现的。

深度的解决方案应该是控制队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员,另一个解决方案是任意异步调用都应该包含超时机制,一旦在限定时间内未完成响应,通过回调函数传递异常,完成回落,使得任意异步调用都具备可控的响应时间,给消费速度一个下限值。

内存泄漏排查

现在有许多工具用于定位Node的内存泄漏,下面介绍其中的2种,node-heapdump 和 node-memwatch

node-heapdump

安装:npm install heapdump

使用:

var heapdump = require('heapdump'); // 引入
var http = require('http');
// 写一份内存泄漏的代码
var leakArray = [];
var leak = function () {
  leakArray.push(new Array(2 * 1024 * 1024));
};
http.createServer(function (req, res) {
  leak();
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337);

上述代码在使用node跑起来之后,每次请求localhost:1337都会使得leakArray数组中的元素增加,而得不到回收,我们在多次访问之后,leakArray的每一项都是较大的数组,这时候输入命令可以使用headdump抓拍一份堆内存的快照。

1、使用 lsof -i:1337 查看1337端口上的进程的pid
2、输入以下命令抓取
kill -USR2 <pid>  // pid 进程号

这份抓取的快照会在文件目录下以heapdump-<sec>.<usec>.heapsnapshot的格式存放,这是一份较大的JSON文件,需要通过Chrome的开发者工具打开查看,开发者工具 - Memory - Profiles - 点击右下角的load打开刚才的快照文件,就可以查看堆内存中的详细信息,可以查看内存分布,可以找出泄漏的数据,然后根据这些信息找到泄漏的代码。

heapdump.png

node-memwatch

安装: npm install memwatch

使用:

var memwatch = require('memwatch');
var http = require('http');

memwatch.on('leak', function (info) {
  console.log('leak:');
  console.log(info);
});
memwatch.on('stats', function (stats) {
  console.log('stats:')
  console.log(stats);
});

var leakArray = [];
var leak = function () {
  leakArray.push("leak" + Math.random());
};

http.createServer(function (req, res) {
  leak();
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337);
console.log('Server running at http://127.0.0.1:1337/');

在进程使用node-memwatch之后,每次进行全堆垃圾收集时,将会触发一次stats事件,这个事件将会传递内存的统计信息,在对上述代码创建的服务进程进行访问时,某次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  // 最大
}

如果经过连续5次垃圾回收后,内存仍然没有得到释放,这意味着有内存泄漏的产生,这时候node-memwatch会触发一个leak事件,某次leak事件得到的数据如下:

leak:
{ 
  start: Mon Oct 07 2013 13:46:27 GMT+0800 (CST),
  end: Mon Oct 07 2013 13:54:40 GMT+0800 (CST),
  growth: 6222576,
  reason: 'heap growth over 5 consecutive GCs (8m 13s) - 43.33 mb/hr' 
}

这个数据能显示5次垃圾回收的过程中内存增长了多少,而具体问题产生在何处还需要从V8堆内存上定位,node-memwatch提供了抓取快照和比较快照的功能,它能够比较堆上对象的名称和分配数量,从而找到导致内存泄漏的元凶。

var memwatch = require('memwatch');
var leakArray = [];

var leak = function () {
  leakArray.push("leak" + Math.random());
};
// 抓取第一次
var hd = new memwatch.HeapDiff();
for (var i = 0; i < 10000; i++) {
leak();
}
// 抓取第二次并进行比较得出diff
var diff = hd.end();
console.log(JSON.stringify(diff, null, 2));

运行以上代码,得到json字符串如下。

{
  "before": {
    "nodes": 11719,
    "time": "2013-10-07T06:32:07.000Z",
    "size_bytes": 1493304,
    "size": "1.42 mb"
  },
  "after": {
    "nodes": 31618,
    "time": "2013-10-07T06:32:07.000Z",
    "size_bytes": 2684864,
    "size": "2.56 mb"
  },
  "change": {
    "size_bytes": 1191560,
    "size": "1.14 mb",
    "freed_nodes": 129,
    "allocated_nodes": 20028,
    "details": [
      {
        "what": "Array",
        "size_bytes": 323720,
        "size": "316.13 kb",
        "+": 15,
        "-": 65
      },
      {
        "what": "Code",
        "size_bytes": -10944,
        "size": "-10.69 kb",
        "+": 8,
        "-": 28
      },
      {
        "what": "String",
        "size_bytes": 879424,
        "size": "858.81 kb",
        "+": 20001,
        "-": 1
      }
    ]
  }
}

其中change节点下的freed_nodes和allocated_nodes,记录了释放的节点数量和分配的节点数量,由于内存泄漏,分配的节点数量远远多余释放的节点数量。

在detail数组可以看出每种类型的分配和释放数量,+ 和 - 号分别表示分配和释放的对象数量,其中可以看出有大量的字符串没有被回收。

大内存应用

由于Node的内存限制,操作大文件也需要小心,好在Node提供了stream模块用于处理大文件。

stream是Node原生模块,继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法, 在Node中大多数模块都有stream的应用,比如fs的createReadStream()和createWriteStream()方法可以分别用于创建文件的可读流和可写流,process模块中stdin和stdout分别是可读流和可写流的示例。

由于V8的内存限制,我们无法通过fs.readFile()和fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()和fs.createWriteStream()方法通过流的方式实现对大文件的操作。

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();
});

// 或者简写为

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);

可读流提供了管道方法pipe(),封装了data事件和写入操作,通过流的方式,上述代码不会受到V8内存限制的影响,有效的提高了程序的健壮性。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容