V8 的 Memory Scheme (内存模型)
一个运行中的程序总是与内存中的一部分空间相对应。这部分空间叫做 Resident Set (驻留集)。V8 组织内存的方式与 Java 类似 (如下图)。
其中
- Code Segment 存放正在被执行的代码
- Stack 中存放了原始类型变量(比如 Boolean)以及指针(指向存在 Heap 中的 Object 或用于程序的控制流)
- Heap 中存放了引用类型的变量(比如 Object、String 或 Closure)
在 nodejs 中可以使用 process.memoryUsage() 查看当前程序的内存使用情况。 该函数返回
- rss: Resident Set 的大小
- heapTotal: 堆的总大小
- heapUsed: 实际使用的堆大小
没有返回 Stack 的原因大概是通常因为这部分所占内存比较小(?)
正常的一直运行的代码,内存占用图应该是如下图所示。其中最上面的橙线代表 rss, 中间的红线代表 heapTotal,黄线代表 heapTotal。可以看到黄线随时间变化震荡比较激烈,这是垃圾回收在起作用。
内存的生命周期
- 分配内存: 在定义变量,函数;或是调用某些函数(比如
new Date()
)时分配 - 使用分配到的内存(读、写): 使用变量,对变量赋值,调用函数等情况
- 不需要时将其释放\归还:垃圾回收
垃圾回收机制
定义:释放某段不再被引用的内存的算法。
现代 js 引擎使用的垃圾回收方法:mark and sweep
这个算法描述起来并不复杂:
假定已知一个根对象(root) (在 js 里,root 就是全局对象)。垃圾回收器会周期性地从 root 开始,寻找所有 root 引用的对象,然后找这些对象引用的对象……从 root 开始,垃圾回收器将找到所有可以获得的对象,然后将无法获得的对象回收。
举个例子来说:
// 定义函数(分配内存)
var run = function () {
// 定义一个巨大的数组
var str = new Array(1000000).join('*');
// 定义一个使用 str 的 闭包
var doSthWithStr = function () {
if (str === 'something')
console.log("Hi there");
};
// 调用这个闭包
doSthWithStr();
};
// 每隔 1 s 调用 run 函数,因此 run 不会被回收。
// 每次调用完 run 之后,由于没有外部对象引用,内部的变量和闭包会被回收
// 所以不会出现内存一直增长的问题。
setInterval(run, 1000);
内存泄漏
定义: 不断使用新的内存,旧的又没有回收,导致程序的内存占用不断增长
例子:稍微改写下上面的代码:
var run = function () {
var str = new Array(1000000).join('*');
var doSthWithStr = function () {
if (str !== 'something')
console.log("Hi there");
};
// 由于 doSthWithStr 作为回调函数传给了 setInterval,所以不会被回收
// 而 str 在它的词法作用域中,并且在函数内部有调用,所以也不会被回收(闭包)
// 因此内存占用会一直增长
setInterval(doSthWithStr, 100);
};
setInterval(run, 1000);
你可以把上述代码粘贴到 chrome 的 console 中运行,然后打开 timeline tab,录制内存使用量。你会看到内存使用量以每秒 1 M 的速度增长
Refs
http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management