Node 内存控制

JavaScript开发者很少在开发过程中遇到需要对内存进行控制的场景,也缺乏控制的手段。随着Node的发展,JavaScript的应用场景已经不局限于浏览器中,寸土寸金的服务器端要实现为海量用户服务,就得使一切资源要高效利用。

V8的内存限制

JavaScript与Java一样通过垃圾回收机制来进行自动内存管理,这使开发者不必时刻关注内存分配和释放的问题。

V8的内存分配

在一般的后端开发语言中,系统对内存使用基本没什么限制,然而Node只能使用部分内存(64位系统约1.4GB,32位系统约为0.7GB)。在这样的限制下,Node无法直接操作大内存对象。这个问题主要原因是Node基于V8构建,V8这套内存管理机制在浏览器上绰绰有余,但是在Node上,却限制了开发者使用大内存的想法。

高效使用内存

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

作用域

JavaScript能形成作用域的有函数调用、with、全局作用域:

1
2
3
var foo = function () {
var local = {}
}

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

标识符查找

所谓标识符,可以理解为变量名,执行下面函数时,会遇到local变量:

1
2
3
var foo = function () {
console.log(local)
}

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

作用域链

1
2
3
4
5
6
7
8
9
10
11
12
13
var foo = function () {
var local='local val'
var bar=function (){
var local='another val'
var baz=function(){
console.log(local)
}
baz()
}
bar()
}
foo()
// another val

当我们在baz函数中访问local变量时,由于作用域中的列表没有local,所以会向上一个作用域查找,接着会在bar函数执行得到的变量列表中找到local的定义,于是使用它。尽管在更上一层的作用域中也存在local的定义,但是不会继续查找了。

变量的主动释放

如果变量时全局变量,由于全局作用域要直到进程退出才能释放,此时会导致引用的对象常驻内存。如果需要释放常驻内存的对象,可以通过delete来释放。

1
2
3
global.foo="I am global object"
console.log(global.foo)
delete global.foo

闭包

作用域链上的对象访问只能向上,这样外部无法访问内部:

1
2
3
4
5
6
7
8
var foo = function () {
(function () {
var local = "local val";
})()
console.log(local);
}
foo()
// Uncaught ReferenceError ReferenceError: local is not defined

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

1
2
3
4
5
6
7
8
9
10
11
var foo = function () {
var bar = function () {
var local = "local val"
return function () {
return local;
}
}
var baz = bar()
console.log(baz())
}
foo()

一般而言,在bar函数执行完后,局部变量local会随着作用域的销毁而被回收,但是这里的返回值是一个匿名函数,这个函数具备了访问local的条件,虽然在后续的执行中,外部作用域还是无法直接访问local,但是可以通过这个中间函数周转即可访问。

闭包是JavaScript的特性,它的问题在于,一旦有变量引用这个中间函数,这个中间函数不会被释放,同时也会使原始的作用域不会得到释放。

内存指标

查看内存使用的情况

调用process.memoryUsage()可以查看内存的使用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var showMem = function () {
var mem = process.memoryUsage()
var format = function (bytes) {
return (bytes / 1024 / 1024).toFixed(2) + 'MB'
}
for (const key in mem) {
if (Object.hasOwnProperty.call(mem, key)) {
const element = mem[key];
mem[key] = format(element)
}
}
console.log(mem);
}
showMem()
/*
{
rss: '19.35MB',
heapTotal: '4.77MB',
heapUsed: '3.99MB',
external: '0.31MB',
arrayBuffers: '0.01MB'
}
*/

写一个方法用于不停地分配内存但不释放内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 ${mem.rss}`)
console.log("--------------------------------------");
}

var useMen = () => {
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 (let index = 0; index < 15; index++) {
showMem()
total.push(useMen())
}

showMem()

Process:heapTotal 4.77MB,heapUsed 3.99MB,rss 19.44MB
--------------------------------------
Process:heapTotal 164.78MB,heapUsed 164.04MB,rss 181.47MB
--------------------------------------
Process:heapTotal 325.79MB,heapUsed 323.87MB,rss 341.90MB
--------------------------------------
Process:heapTotal 488.54MB,heapUsed 483.89MB,rss 502.35MB
--------------------------------------
Process:heapTotal 652.55MB,heapUsed 643.85MB,rss 662.57MB
--------------------------------------
Process:heapTotal 820.56MB,heapUsed 803.85MB,rss 823.16MB
--------------------------------------
Process:heapTotal 996.57MB,heapUsed 963.85MB,rss 983.77MB
--------------------------------------
Process:heapTotal 1156.07MB,heapUsed 1123.36MB,rss 1143.75MB
--------------------------------------
Process:heapTotal 1316.08MB,heapUsed 1283.43MB,rss 1303.76MB
--------------------------------------
Process:heapTotal 1476.09MB,heapUsed 1443.64MB,rss 1463.77MB
--------------------------------------
Process:heapTotal 1636.10MB,heapUsed 1603.64MB,rss 1623.78MB
--------------------------------------
Process:heapTotal 1796.11MB,heapUsed 1763.64MB,rss 1783.84MB
--------------------------------------
Process:heapTotal 1956.11MB,heapUsed 1923.64MB,rss 1943.85MB
--------------------------------------
Process:heapTotal 2116.12MB,heapUsed 2083.36MB,rss 2103.86MB
--------------------------------------
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

可以看到每次调用useMem都导致了三个值的增长,循环只执行了14次,在2000MB左右的时候,无法继续分配内存,进程内存溢出。

堆外内存

将前面的useMem中的Array改成Buffer,然后再次运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var useMen = () => {
var size = 20 * 1024 * 1024
var arr = new Buffer(size)
for (var i = 0; i < size; i++) {
arr[i] = 0;
}
return arr;
}

Process:heapTotal 4.77MB,heapUsed 3.99MB,rss 19.43MB
--------------------------------------
Process:heapTotal 5.27MB,heapUsed 4.56MB,rss 41.88MB
--------------------------------------
Process:heapTotal 5.27MB,heapUsed 4.57MB,rss 61.89MB
--------------------------------------
Process:heapTotal 6.27MB,heapUsed 4.40MB,rss 82.41MB
--------------------------------------
Process:heapTotal 5.77MB,heapUsed 4.40MB,rss 102.50MB
--------------------------------------
Process:heapTotal 5.77MB,heapUsed 4.40MB,rss 122.55MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.50MB,rss 142.59MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.88MB,rss 162.61MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.89MB,rss 182.61MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.88MB,rss 202.64MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.88MB,rss 222.69MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.88MB,rss 242.69MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.51MB,rss 262.70MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.51MB,rss 282.70MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.88MB,rss 302.71MB
--------------------------------------
Process:heapTotal 6.77MB,heapUsed 3.88MB,rss 322.71MB
--------------------------------------

可以看到15次循环都完整运行,并且heapTotal和heapUsed变化极小,唯一变化的是rss值。原因是Buffer对象不同于其他对象,它不经V8的内存分配机制,所以也不会有内存的大小限制。

从上面的例子可以得知Node的内存构成主要通过V8进行分配的部分和Node自行分配的部分,受V8的垃圾回收限制的是V8的堆内存。

内存泄露

V8的垃圾回收机制下,在通常的代码编写中,很少出现内存泄露的情况。但是内存泄漏通常产生于无意中,难以排查。通常,造成内存泄漏的原因有如下几个:

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

慎将内存当缓存

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

下面代码利用对象十分容易创建一个缓存对象,但是受垃圾回收机制的影响,只能小量使用:

1
2
3
4
5
6
7
8
9
10
11
var cache = {}
var get = (key) => {
if (cache[key]) {
return cache[key]
} else {
//get from otherwise
}
}
var set = (key, value) => {
cache[key] = value
}

所以在Node中,任何试图拿内存当缓存的行为都应该被限制,小心使用。

缓存限制策略

为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var LimitTableMap = function (limit) {
this.limit = this.limit || 10
this.map = {}
this.keys = []
}
var hasOwnProperty = Object.prototype.hasOwnProperty

LimitTableMap.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)
}
keys[key] = value
}

LimitTableMap.prototype.get = function (key) {
return this.map[key]
}

实现过程还是比较简单的,当然这种策略并不是十分高效,只能应付小型应用场景。

缓存的解决方案

直接拿内存做为缓存要十分慎重,除了限制缓存大小外,还要考虑到进程间无法共享内存。如何使用缓存,比较好的方法是采用进程外的缓存,如Redis和Memcached。

大内存应用

在Node中,不可避免地会出现操作大文件地场景,由于Node的内存限制,操作大文件也需要小心,Node提供了Stream用于处理大文件:

1
2
3
4
5
6
7
8
9
var fs = require("fs")
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()
})

Node可读流提供了管道pipe方法,封装了data事件和写入操作:

1
2
3
4
var fs = require("fs")
var reader = fs.createReadStream('in.txt')
var writer = fs.createWriteStream('out.txt')
reader.pipe(writer)

虽然这时代码不会受到V8的内存限制,但是依然要小心,物理内存依然有限制。