0.背景
- 如网页应用,命令行工具等短时间执行的场景,随着进程的退出,内存会释放,几乎没有内存管理的必要。
- 基于无阻塞,事件驱动的node服务,具有内存消耗低的优点,非常适合处理海量的网络请求。
在服务端,资源向来都是寸土寸金,要为海量用户服务,就要使一切资源都要高效循环利用。在海量请求和长时间运行下,内存控制尤为重要!
一.V8的内存限制(内存的事实现状)
Node中通过javascript使用内存并不能达到物理内存的上限,实际上使用的在64位系统下约1.4G,32位系统下约0.7G。
因为Node基于V8创建,V8有自己的方式分配和管理内存。
- 表层原因:V8最初为浏览器而设计,不太可能遇到使用大量内存的场景;
- 深层原因: V8的垃圾回收机制的限制(以“非增量式”的回收1.5G的堆内存需要javascript线程暂停要1秒以上)
二.V8的对象分配(何时会遇到内存限制的状况?)
V8的所有jacascript对象都是通过堆来进行分配的。
当我们在代码中声明变量并赋值时,所使用的对象的内存就分配在堆内存中。如果已经申请的堆内存不够分配新的对象,则继续申请堆内存,直到堆的大小超过V8的限制。
//查看进程的内存使用量
process.memoryUsage();
{ rss: 23236608, //常驻内存
heapTotal: 9682944, //申请到的堆内存
heapUsed: 5559752, //已使用的堆内存
external: 10132
}
//查看系统的内存使用量
> os.totalmem();
8418570240
> os.freemem();
2272493568
当然,V8在Node启动时可通过传递参数,来突破该内存限制:
node --max-old-space-size=1700 index.js //1700MB
//or
node --max-new-space-size=1024 index.js //1400KB
在Node运行时,不能动态修改!
三.V8的垃圾回收机制(V8为何有内存限制?)
V8的垃圾回收策略主要是分代式垃圾回收机制
。所谓分代,即将内存分为新生代和老生代两代。
- 新生代中的对象:存活时间较短的对象;
- 老生代中的对象:存活时间较长的对象或常驻内存的对象。
V8堆的大小 = 新生代内存空间 + 老生代内存空间
1.新生代的垃圾回收(Scavenge算法)
解析:
- 新生代内存空间均分为二,每部分成为semispace。在某一时刻只有一个处于使用中(From空间),另一个处于闲置状态(To空间);
- 分配对象时,先在From空间进行分配;
- 开始垃圾回收时,检查From空间的存活对象并分配到To空间,非存活对象占用的空间被释放,完成复制后,From空间和To空间角色互换。
V8堆的大小 = 新生代内存空间(From空间 + To空间) + 老生代内存空间
评价:
- 典型的牺牲空间换时间:仅仅使用了新生代堆内存空间的一半,(由于生命周期短的场景中存活对象只占少部分)因此复制时时间效率很高;
2.新生代对象向老生代对象的'晋升'
当一个对象复制多次依然存活时,将会被认为时生命周期长的对象。该对象随后(检查From空间时)会被移动到老生代中,晋升流程:
- From空间中,该对象是否经历过Scavenge回收?是,进入老生代;否,进入To空间;
- From空间中,判断To空间使用率是否超过25%?是,进入老生代;否,进入To空间;
3.老生代的垃圾回收(Mark-Sweep & Mark-Compact)
老生代中垃圾回收是将Mark-Sweep和Mark-Compact相结合。
Mark-Sweep(标记清除)算法解析:
- 标记阶段:遍历堆中所有对象,并标记活着的对象;
- 清除阶段: 只清除没有标记的对象。
评价:
- 最大的问题是每次标记清除回收空间后,内存空间会出现不连续。这种内存碎片会对随后的内存分配造成问题,假如需要分配一个大对象,如果所有碎片空间都无法完成此次分配,则会提前触发不必要的垃圾回收。
Mark-Compact(标记整理)算法解析:
- 标记阶段:编理堆中所有对象,标记死亡对象;
- 整理阶段: 将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存;
4. 增量标记
'全停顿':以上垃圾回收的三种方式中,为了避免javascript逻辑和垃圾回收器看到的不一致,需要将javascript暂停下来,执行完垃圾回收后在恢复执行javascript逻辑。
为了降低该停顿时间,V8在标记阶段,将原本需要一口气停顿完成的动作改为'增量标记':拆分成许多小“步进”,每完成一个小步进,就让javascript逻辑执行一会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。
此外,在清除和整理阶段,V8也逐步引入增量式。
5. 查看垃圾回收的效率
(待)
四. 高效使用内存(实际编码中,如何让垃圾回收机制高效工作呢?)
在正常的javascript执行中,无法立即回收的内存有闭包和全局变量引用。因为这两种将导致老生代中的对象增多。
1.闭包
通常作用域链上的对象只能向上访问,外部无法访问内部。
var local = '局部变量';
(function() {
console.log(local);
}())
//正常打印
(function() {
var local = '局部变量';
}())
console.log(local);
//抛出异常
'闭包': javascript中,实现外部作用域访问内部作用域中变量的方法叫做闭包。
这得益于高阶函数的特性:可以作为参数或返回值。
var bar = function() {
var local = '局部变量';
return function() {
return local;
}
}
var baz = bar();
console.log(baz());
特点是:
- bar()的返回值是一个匿名函数,该函数具备了访问local的条件;
- 若要访问local,只要通过bar()这个中间函数稍作周转即可;
闭包对内存回收机制的影响:
一旦有变量引用这个中间函数,该中间函数将不会释放,同时也会是原始的作用域不会得到释放,作用域中产生的内存占用也得不到释放。除非不再引用,才会逐步释放。
2.作用域
如果变量是全局变量(不通过var声明或直接定义在global变量上),由于全局作用域需要直到进程退出才会释放,此时将导致引用的对象常驻内存(即常驻在老生代内存中)。
只有通过delete删除引用关系或将变量重新赋值,让旧的对象脱离引用关系,才能在接下来的老生代内存清除和整理的过程中,被回收释放。
global.foo = 'globalData';
console.log(global.foo);
delete global.foo;
//或者
global.foo = undefined/null;
console.log(global.foo); //此后即可垃圾回收
五. 堆外内存(引申)
通过process.memoryUsage()的结果可看出,堆中内存使用量总是小于进程的常驻内存量。
实际上,Node的内存,主要是通过V8进行分配的堆内存和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;
}
//headTotal 3.86MB heapUsed 2.07MB rss 11.12MB
//...
//headTotal 5.85MB heapUsed 1.85MB rss 3012.91MB
以上可看出Buffer对象不同于其它对象,它不经过V8的内存分配机制,因此不会有堆内存的大小限制。也意味着利用堆内存可以突破内存限制的问题。
注:以上均是自己技术栈的整理,仅供备忘。如需交流:stevekeol(微信号)
网友评论