内存控制
内存控制正是在海量请求和长时间运行的前提下进行探讨的。在服务器端,资源向来就寸土寸金,要为海量用户服务,就得使一切资源都要高效循环利用。
# 01. V8 的垃圾回收机制与内存限制
JavaScript由垃圾回收机制来进行自动内存管理,
# V8 的内存限制
在Node中通过JavaScript使用内存时就会发现只能使用部分内存(64位系统下约为1.4 GB,32位系统下约为0.7 GB)。 造成这个问题的主要原因在于Node基于V8构建,所以在Node中使用的JavaScript对象基本上都是通过V8自己的方式来进行分配和管理的。V8的这套内存管理机制在浏览器的应用场景下使用起来绰绰有余,足以胜任前端页面中的所有需求。但在Node中,这却限制了开发者随心所欲使用大内存的想法.
# V8 的对象分配
在V8中,所有的JavaScript对象都是通过堆来进行分配的。Node提供了V8中内存使用量的查看方式,执行下面的代码,将得到输出的内存信息
wsh@wsh ~ % node
> process.memoryUsage();
{
rss: 99205120,
heapTotal: 6049792,
heapUsed: 4121312,
external: 1682060,
arrayBuffers: 9965
}
>
当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制为止。
V8为何要限制堆的大小?
- 表层原因: 为V8最初为浏览器而设计,不太可能遇到用大量内存的场景
- 深层原因是V8的垃圾回收机制的限制。按官方的说法,以1.5 GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。这样的情况不仅仅后端服务无法接受,前端浏览器也无法接受。因此,在当时的考虑下直接限制堆内存是一个好的选择。
调整内存限制的大小
node --max-old-space-size=1700 test.js // 单位为MB
// 或者
node --max-new-space-size=1024 test.js // 单位为KB
# V8 的垃圾回收机制
# V8主要的垃圾回收算法
V8的垃圾回收策略主要基于分代式垃圾回收机制。
- V8的内存分代 在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
V8的分代示意图:
V8堆的整体大小就是新生代所用内存空间加上老生代的内存空间
- --max-old-space-size命令行参数可以用于设置老生代内存空间的最大值,
- --max-new-space-size命令行参数则用于设置新生代内存空间的大小
# 新生代内存
对于新生代内存,它由两个reserved_semispace_size_所构成。按机器位数不同,reserved_semispace_size_在64位系统和32位系统上分别为16 MB和8 MB。所以新生代内存的最大值在64位系统和32位系统上分别为32 MB和16 MB。
V8堆内存的最大保留空间可以从下面的代码中看出来,其公式为4 * reserved_semispace_size_ + max_old_generation_size_。 因此,默认情况下,V8堆内存的最大值在64位系统上为1464 MB,32位系统上则为732 MB。这个数值可以解释为何在64位系统下只能使用约1.4 GB内存和在32位系统下只能使用约0.7 GB内存。
- Scavenge算法
在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用了
Cheney算法
. Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为semispace.
在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象
,这
些存活对象将被复制到To空间
中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制。
V8的堆内存示意图
Scavenge的缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异的表现。适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰 适合这个算法
晋升
: 当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。对象从新生代中移动到老生代中的过程称为晋升。
在分代式垃圾回收的前提下,From空间中的存活对象在复制到To空间之前需要进行检查。在一定条件下,需要将存活周期长的对象移动到老生代中,也就是完成对象晋升. 对象晋升的条件主要有两个:
- 一个是对象是否经历过Scavenge回收
晋升流程: 2. 一个是To空间的内存占用比超过限制。
晋升的判断示意图
设置25%这个限制值的原因是当这次Scavenge回收完成后,这个To空间将变成From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配.
# 老生代内存 (Mark-Sweep(标记清除) & Mark-Compact(标记整理))
Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段 Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。 Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象。活对象在新生代中只占较小部分,死对象在老生代中只占较小部分。 Mark-Sweep在老生代空间中标记后的示意图:黑色部分标记为死亡的对象
Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态
。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间
都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
Mark-Compact: 在Mark-Sweep的基础上演变而来的。是为了解决Mark-Sweep的内存碎片问题。它们的差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。白色格子为存活对象,深色格子为死亡对象,浅色格子为存活对象移动后留下的空洞。
完成移动后,就可以直接清除最右边的存活对象后面的内存区域完成回收。
完成标记并移动存活对象后的示意图:
3种垃圾回收算法的简单对比:
从表中可以看到,在Mark-Sweep和Mark-Compact之间,由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,所以在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时才使用Mark-Compact。
# 增量标记
为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为全停顿
。
在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。
但V8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full 垃圾回收)的标记、清理、整理等 动作造成的停顿就会比较可怕,需要设法改善。
增量标记
: 为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记。
也就是拆分为许多小“步进”,每做完一“步进”就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。
# 查看垃圾回收日志
查看垃圾回收日志的方式主要是在启动时添加--trace_gc参数。在进行垃圾回收时,将会从标准输出中打印垃圾回收的日志信息。
node --trace_gc -e "var a = [];for (var i = 0; i < 1000000; i++) a.push(new Array(100));"
[71982:0x118008000] 26 ms: Scavenge 2.4 (3.0) -> 2.0 (4.0) MB, 0.7 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 33 ms: Scavenge 2.6 (4.3) -> 2.5 (5.0) MB, 0.6 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 34 ms: Scavenge 3.2 (7.0) -> 3.2 (7.3) MB, 0.3 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 35 ms: Scavenge 4.5 (7.3) -> 4.5 (7.8) MB, 0.4 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 36 ms: Scavenge 5.1 (11.8) -> 4.9 (13.0) MB, 0.8 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 37 ms: Scavenge 8.3 (13.0) -> 8.6 (13.8) MB, 0.8 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 39 ms: Scavenge 8.8 (21.8) -> 8.4 (25.5) MB, 1.2 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 50 ms: Scavenge 16.3 (25.7) -> 17.1 (26.5) MB, 3.2 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 52 ms: Scavenge 17.1 (42.5) -> 16.1 (49.5) MB, 2.5 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 64 ms: Scavenge 32.7 (50.1) -> 34.3 (51.9) MB, 6.8 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 69 ms: Scavenge 34.3 (51.9) -> 32.3 (65.9) MB, 4.2 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 99 ms: Mark-sweep 86.4 (111.6) -> 85.0 (118.8) MB, 3.0 / 0.0 ms (+ 8.1 ms in 127 steps since start of marking, biggest step 0.2 ms, walltime since start of marking 23 ms) (average mu = 1.000, current mu = 1.000) finalize incremental marking via stack guard GC in old space requested
[71982:0x118008000] 437 ms: Scavenge 482.6 (517.2) -> 476.3 (511.0) MB, 35.8 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[71982:0x118008000] 495 ms: Mark-sweep 544.1 (579.1) -> 542.6 (577.6) MB, 6.1 / 0.0 ms (+ 70.0 ms in 711 steps since start of marking, biggest step 0.4 ms, walltime since start of marking 235 ms) (average mu = 0.808, current mu = 0.808) finalize incremental marking via stack guard GC in old space requested
V8提供了linux-tick-processor工具用于统计日志信息。该工具可以从Node源码的 deps/v8/tools目录下找到,Windows下的对应命令文件为windows-tick-processor.bat。将该目录添 加到环境变量PATH中,即可直接调用:
linux-tick-processor v8.log
# 02. 高效使用内存
在V8面前,开发者所要具备的责任是如何让垃圾回收机制更高效地工作。
# 作用域
提到如何触发垃圾回收,第一个要介绍的是作用域(scope)。在JavaScript中能形成作用域 的有函数调用、with以及全局作用域.
以如下代码为例:
var foo = function () {
var local = {};
};
foo()函数在每次被调用时会创建对应的作用域,函数执行结束后,该作用域将会销毁。同时作用域中声明的局部变量分配在该作用域上,随作用域的销毁而销毁。只被局部变量引用的对象存活周期较短。在这个示例中,由于对象非常小,将会分配在新生代中的From空间中。在作用域释放后,局部变量local失效,其引用的对象将会在下次垃圾回收时被释放。
以上就是最基本的内存回收过程。
- 标识符查找 所谓标识符,可以理解为变量名
在下面的代码中,执行 bar()函数时,将会遇到local变量:
var bar = function () {
console.log(local);
};
JavaScript在执行时会去查找该变量定义在哪里。它最先查找的是当前作用域,如果在当前作 用域中无法找到该变量的声明,将会向上级的作用域里查找,直到查到为止。
- 作用域链 在下面的代码中:
var foo = function () {
var local = "local var";
var bar = function () {
var local = "another var";
var baz = function () {
console.log(local);
};
baz();
};
bar();
};
foo();
变量在作用域中的查找示意图
- 变量的主动释放 如果变量是全局变量(不通过var声明或定义在global变量上),由于全局作用域需要直到进程退出才能释放,此时将导致引用的对象常驻内存(常驻在老生代中)。如如果需要释放常驻内存的对象,可以通过delete操作来删除引用关系。或者将变量重新赋值,让旧的对象脱离引用关系。在接下来的老生代内存清除和整理的过程中,会被回收释放。 下面为示例代码:
global.foo = "I am global object";
console.log(global.foo); // => "I am global object"
delete global.foo;
// 或者重新赋值
global.foo = undefined; // or null
console.log(global.foo); // => undefined
提示
是在V8中通过delete删除对象的属性有可能干扰V8的优化,所以通过赋值方式解除引用更好。
# 闭包
作用域链上的对象访问只能向上,这样外部无法向内部访问
var foo = function () {
var local = "局部变量";
(function () {
console.log(local);
}());
};
输出:
局部变量
但在下面的代码中,却会得到local未定义的异常:
var foo = function () {
(function () {
var local = "局部变量";
}());
console.log(local);
};
输出异常:
console.log(local);
^
ReferenceError: local is not defined
at foo (/Users/wsh/github/node-demo/index.js:68:15)
at Object.<anonymous> (/Users/wsh/github/node-demo/index.js:70:1)
在JavaScript中,实现外部作用域访问内部作用域中变量的方法叫做闭包
。这得益于高阶函数的特性:函数可以作为参数或者返回值。示例代码的如下:
var foo = function () {
var bar = function () {
var local = "局部变量";
return function () {
return local;
};
};
var baz = bar();
console.log(baz());
};
一般而言,在bar()函数执行完成后,局部变量local将会随着作用域的销毁而被回收。注意返回值是一个匿名函数,且这个函数中具备了访问local的条件,虽然在后续的执行中,在外部作用域中还是无法直接访问local,但是若要访问它,只要通过这个中间函 数稍作周转即可。
一旦有变量引用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放。除非不再有引用,才会逐步释放。
总结
在正常的JavaScript执行中,无法立即回收的内存有闭包和全局变量引用这两种情况。由于V8的内存限制,要十分小心此类变量是否无限制地增加,因为它会导致老生代中的对象增多。
# 03. 内存指标
一般而言,应用中存在一些全局性的对象是正常的,而且在正常的使用中,变量都会自动释放回收。但是也会存在一些我们认为会回收但是却没有被回收的对象,这会导致内存占用无限增长。一旦增长达到V8的内存限制,将会得到内存溢出错误,进而导致进程退出。
# 查看内存使用情况
- process.memoryUsage()可以查看内存使用情况
- os模块中的totalmem()和freemem()方法也可以查看内存使用情况
# 查看进程的内存占用
调用process.memoryUsage()可以看到Node进程的内存占用情况,示例代码如下:
wsh@wsh ~ % node
> process.memoryUsage()
{
rss: 118702080,
heapTotal: 5005312,
heapUsed: 3128976,
external: 1672670,
arrayBuffers: 9911
}
- rss是resident set size的缩写,即进程的常驻内存部分。进程的内存总共有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)中。
- heapTotal和heapUsed对应的是V8的堆内存信息。heapTotal是堆中总共申请的内存量,heapUsed表示目前堆中使用中的内存量。这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("-----------------------------------------------------------");
};
showMem();
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();
输出结果:JavaScript 堆内存不足
Process: heapTotal 4.27 MB heapUsed 2.59 MB rss 90.94 MB
-----------------------------------------------------------
Process: heapTotal 4.77 MB heapUsed 2.93 MB rss 94.16 MB
-----------------------------------------------------------
Process: heapTotal 164.82 MB heapUsed 162.94 MB rss 749.16 MB
-----------------------------------------------------------
Process: heapTotal 325.82 MB heapUsed 322.71 MB rss 1391.83 MB
-----------------------------------------------------------
Process: heapTotal 488.08 MB heapUsed 482.73 MB rss 2035.33 MB
-----------------------------------------------------------
Process: heapTotal 652.58 MB heapUsed 642.70 MB rss 2654.91 MB
-----------------------------------------------------------
Process: heapTotal 820.59 MB heapUsed 802.70 MB rss 3097.00 MB
-----------------------------------------------------------
Process: heapTotal 996.59 MB heapUsed 962.70 MB rss 3723.53 MB
-----------------------------------------------------------
Process: heapTotal 1156.59 MB heapUsed 1122.70 MB rss 4335.13 MB
-----------------------------------------------------------
Process: heapTotal 1316.60 MB heapUsed 1282.70 MB rss 4932.05 MB
-----------------------------------------------------------
Process: heapTotal 1476.60 MB heapUsed 1442.70 MB rss 5535.61 MB
-----------------------------------------------------------
Process: heapTotal 1636.61 MB heapUsed 1602.70 MB rss 6146.20 MB
-----------------------------------------------------------
Process: heapTotal 1796.61 MB heapUsed 1762.70 MB rss 6786.23 MB
-----------------------------------------------------------
Process: heapTotal 1956.61 MB heapUsed 1922.70 MB rss 7426.31 MB
-----------------------------------------------------------
Process: heapTotal 2115.87 MB heapUsed 2082.05 MB rss 8099.53 MB
-----------------------------------------------------------
<--- Last few GCs --->
[75211:0x118008000] 1433 ms: Scavenge 1762.6 (1796.6) -> 1762.6 (1796.6) MB, 11.7 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
[75211:0x118008000] 1638 ms: Mark-sweep 1922.6 (1956.6) -> 1921.9 (1955.9) MB, 110.0 / 0.1 ms (+ 35.4 ms in 10 steps since start of marking, biggest step 6.0 ms, walltime since start of marking 1348 ms) (average mu = 1.000, current mu = 1.000) alloca
<--- JS stacktrace --->
FATAL ERROR: MarkCompactCollector: young object promotion failed Allocation failed - JavaScript heap out of memory
# 查看系统的内存占用
os模块中的totalmem()和freemem()这两个方法用于查看操作系统的内存使用情况,它们分别返回系统的总内存和闲置内存,以字节为单位.
% node
> os.totalmem()
8589934592
> os.freemem()
110432256
>
从输出信息可以看到我的电脑的总内存为8 GB,当前闲置内存大致为1GB。
# 堆外内存
通过process.momoryUsage()的结果可以看到,堆中的内存用量总是小于进程的常驻内存用量,这意味着Node中的内存使用并非都是通过V8进行分配的。我们将那些不是通过V8分配的内存称为堆外内存
。
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;
};
唯一变化大的是rss的值,原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制,所以也不会有堆内存的大小限制。
总结
Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存
# 04. 内存泄漏
内存泄漏实质
是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象
通常,造成内存泄漏的原因有如下几个。
- 缓存。
- 队列消费不及时。
- 作用域未释放。
# 慎将内存当做缓存
严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。 如果需要,只要限定缓存对象的大小,加上完善的过期策略以防止内存无限制增长,还是可以一用的。 如下代码虽然利用JavaScript对象十分容易创建一个缓存对象,但是受垃圾回收机制的影响, 只能小量使用:
var cache = {};
var get = function (key) {
if (cache[key]) {
return cache[key];
} else {
// get from otherwise
}
};
var set = function (key, value) {
cache[key] = value;
};
- 缓存限制策略 为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长。 模块limitablemap:记录键在数组中,一旦超过数量,就以先进先出的方式进行淘汰
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导出的函数,可以访问文件模块中的私有变量,这样每个文件模块在编译执行后形成的作用域因为模块缓存的原因,不会被释放。由于模块的缓存机制,模块是常驻老生代的 示例代码如下所示:
(function (exports, require, module, __filename, __dirname) {
var local = "局部变量";
exports.get = function () {
return local;
};
});
- 缓存的解决方案
进程之间无法共享内存。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费。
如何使用大量缓存,目前比较好的解决方案是采用进程外的缓存,进程自身不存储状态。外部的缓存软件有着良好的缓存过期淘汰策略以及自有的内存管理,不影响Node进程的性能。它的好处多多,在Node中主要可以解决以下两个问题。
- 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效。
- 进程之间可以共享缓存。
Redis (opens new window) Memcached (opens new window)
# 关注队列状态
在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。但是一旦消费速度低于生产速度, 将会形成堆积。
举个实际的例子,有的应用会收集日志。如果欠缺考虑,也许会采用数据库来记录日志。日志通常会是海量的,数据库构建在文件系统之上,写入效率远远低于文件直接写入,于是会形成数据库写入操作的堆积,而JavaScript中相关的作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏。
的解决方案: 任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限值。
# 05. 内存泄漏排查
在Node中,由于V8的堆内存大小的限制,它对内存泄漏非常敏感。当在线服务的请求量变大时,哪怕是一个字节的泄漏都会导致内存占用过高。 现在已经有许多工具用于定位Node应用的内存泄漏,下面是一些常见的工具。
- node-heapdump。这是Node核心贡献者之一Ben Noordhuis编写的模块,它允许对V8堆内 存抓取快照,用于事后分析。
- node-mtrace。由Jimb Esser提供,它使用了GCC的mtrace工具来分析堆的使用。
- dtrace。在Joyent的SmartOS系统上,有完善的dtrace工具用来分析内存泄漏。
- node-memwatch。来自Mozilla的Lloyd Hilaiel贡献的模块,采用WTFPL许可发布。
# node-heapdump
// 1. 安装node-heapdump
npm install heapdump
// 2. 引入
var heapdump = require('heapdump');
在Chrome的开发者工具中选中Profiles面板,右击该文件后,从弹出的快捷菜单中选择Load... 选项,打开刚才的快照文件,就可以查看堆内存中的详细信息.
# 06. 大内存应用
在Node提供了stream模块
用于处理大文件。stream模块是Node的原生模块,直接引用即可。stream继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。它分可读和可写两种。
- 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堆内存的限制。但是这种大片使用内存的情况依然要小心,即使V8不限制堆 内存的大小,物理内存依然有限制。