您的位置:首页 > Web前端 > Node.js

nodejs的v8引擎垃圾回收机制学习

2014-07-25 23:22 337 查看
nodejs是现在很流行的服务端语言。由于他使用的是JavaScript的语法,所以,很多时候,使用更多的是前端,后端工程师更适应的是比较流行的流式开发。其实它和JAVA类似都是由虚拟机运行,底层虚拟机由C++来实现nodejs的V8虚拟机是Chrome的核心,因为有了它,使得Chrome成为了全世界最快的浏览器之一。并且,由于Node保留了前端的浏览器在JavaScript中那些熟悉的接口和开发方法,所以,前端学习起来基本上是零成本。当然对于后端来讲,熟悉javascript需要一定的时间,主要是它使用的是回调的开发方式和传统的java/c++使用的开发方式不太一样,所以,在编程的习惯上,需要做一个转变

v8虚拟机的开发者是Lars Bak.它原来是Sum公司的工程师,也是负责Java虚拟机的开发,所以,我们可以在V8的虚拟机设计里面看到很多来hotspot虚拟机类似的设计。基本上可以看成是一个简单版的hotspot虚拟机的设计。只不过,Java在虚拟机的设计上本身是为了给服务器运行而开发,针对不同服务器类型可以提供很多优先方案。所以,设计的也比较复杂。nodejs的v8本身设计出来是为了给浏览器运行,所以比较简单。

下面分别从几个方面来介绍一下V8虚拟机的特点

1. 对象分配

在v8里面,所有的Js对象都是直接通过堆来进行分配的。 node也提供了直接的查看方式

process.memoryUsage();




在上图中,能看到,总的堆总容量为500MB,已使用193MB,RSS为进程的常驻内存部分

在v8的堆设计时,限制了堆的大小,64位1.4G、32位0.7G。初始申请的不够会继续申请,只到能申请的最大容量为止,至于为什么限制只能到这个容量,是因为V8最初为设计给浏览器使用,很少会遇到使用大量内存的场景。而且,如果内存申请比较多会导致GC时停止的时间增长,影响正常的服务运行。

当然node也提供了参数来指定大小

node --max-old-space-size=1700 test.js //设置老年代最大内存空间
node --max-new-space-size=1024 test.js //设置新生代最大内存空间


2. 垃圾回收

v8使用分代式垃圾回收机制,这和JAVA的回收算法类似。这种回收机制将内存区分为年轻代和老年代。两个里面存放的对象生命周期不一样。两种分别使用不同的回收算法。

顾名思义,新生代存放的对象生命周期很短,老年代存放的对象生命周期相当长。。前面看到的两个参数就是分别设计这两个参数最大空间的配置了。

年轻代回收算法scavenge: 年轻代回收算法基本上和JAVA的新生代回收算法ParallelScavenge一样。它的使用cheney(强尼)算法:使用复制方式来实现垃圾回。它会将堆内存一分为二,在这两个空间里面,只有一个会使用。另一个闲置,分别是from/to 空间,当我们分配对象时,会在From空间中进行分配,在回收时,会检查from里面存活的对象,然后复制到to空间,非存活的对象会被释放掉。完成后,两个空间的功能会做一个切换。下图为很经典的图


对象是如何释放的呢?
有个叫可达性分析算法的概念,即通过一系列的称为“GC ROOT”的对象作为起始点。从这些节点开始向下搜索。搜索走过的路径称为引用链。当一个对象到GC ROOT没有任何引用链时,则证明此对象是不可用的。当然在虚拟机判断要被释放的对象里面,即使在可达性分析算法中不可达的对象,也并非是立即释放的。如果对象在进行可达性分析后发现没有与GC ROOTS相连接的引用链。将会对它进行一次标记,并进行刷选。它会放进一个队列中依次进行回收。如果这时又有对象引用到它,它就不会被回收了。
如果一个年轻代的对象经过多次复制依然存活。那它就会晋升到老年代里面。当然,如果对象的From空间复制到To空间时,To空间已经使用超过25%时,也会直接晋升到老年代中

老年代回收算法:Mark-Sweep &Mark-Compact:标记-清除。对应到java虚拟机的是老年代算法CMS,CMS相对来说比较复杂,会把整个清除过程分成四个阶段,即:

初始标记:标记GC ROOTS能关联到的对象

并发标记:追踪GC ROOTS的过程
重新标记:修正并发标记时变动对象的标记记录
并发清除:并发的清除

下面举个例子来说明mark-sweep。

a={1,2,3}
a={2,3,4}


这样一段代码在执行了第二行的语句之后,1,2,3这个数组就不会再被引用了,成为GC的对象



从上面这张图可以看清晰的理解到由于a的指针指导向2,3,4前面123没有指针引用。所以,会被回收。

GC会在何时启动呢?一般来说对于虚所机而言,其中一种方法就是在内存不足的时候,即(malloc()返回null时),不过,真到这时候内存已经基本上耗完了。

所以,基本上会在耗费了一定的内在后,就启动GC。我们看一段CHECK GC的实现代码

static void check_gc(CRB_Interpreter *inter)
{
//判断堆的耗费量是否超过阀值
if(inter->heap.current_head_size> inter->head.current_threshold){
crb_garbage_collect(inter);
//设定下一个阀值
inter->heap.current_threshold=inter->heap.current_heap_size+HEAP_THRESHOLD_SIZE;}}


当堆的消耗量超过了当前的阀值就启动GC.GC执行时,current_heap_size的值会变小,然后将变小后的current_heap_size和HEAP_THRESHOLD_SIZE相加,就得出下一个阀值。HEAP_THRESHOLD_SIZE是初始值

至于 crb_garbage_collect(inter );的实现就不细说了,基本上就是标记-清除算法的实现,可以看到代码里面分成两步

void
crb_garbage_collect(CR_Interpreter *inter){
gc_mark_objects(inter);
gc_sweep_objects(inter);

}


与scavenge相比,mark_sweep不会将内存空间分为两半,所以,不会浪费一半空间,它会在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象,所以,和Scavenge相比,标记清除只清理死亡的对象,而标记清除只复制活着的对象。这和新生代堆和老年代堆的特点有关。活的对象在新生代中只占较小的部分,而死的对象在老生代中只占较小部分,所以,这两种方式对于大多数情况下的新生代和老生代都比较高效。

当然。Mark-sweep最大的问题是,在标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题。因此。Mark-Compact被提出。即(标记-整理)它是在前者基础之上演变而来的。让我们来看标记-整理在基维百科上的一张图



可以看到a是未整理前的内存,有三块未被回收的内存对象。在整理的过程中,将活着的对象往左边移动,移动完成以后,直接清理掉边界外的死亡对象,上图中,绿色为存活对象,白色为回收对象,这样完成回收后,内存的空间还是会保护连接的状态。解决了mark-sweep的内存空间不连接的问题。

当然Mark-sweep和Mark-Compact也不是完全可替代的关系。在v8虚拟机中,两者是结合起来使用(这一点HotSpot也是一样)

因为相对前者后者的回收速度是比较慢的,因为它有对象的移动,而mark-sweep没有对象移动,所以,效率会比较高。V8在清理时主要会使用Mark-sweep,在空间不足以对新生代中晋升过来的对象进行分配时才会使用Mark-compact

3. 停顿

以上提到的几种垃圾回收算法都需要将应用逻辑停下来,等完成垃圾回收后再恢复继续执行,即“stop-the-world”,在这点上V8也做了优化。即尽将回收分散,进行增量标记,拆分成许多小“步进”,每做完一“步进”,就让应用逻辑执行一会,垃圾回收和应用逻辑会轮流执行直到标记阶段完成。

4. 实战

最后我们找一些node的垃圾回收代码来学习一下

nohup $NODEJS  --trace_gc --max-old-space-size=200 $NODEJS_SERVER/bin/server.js >$NODE_STDOUT_LOG 2>&1 &


在我们执行node执行时,使用--trace_gc参数,可以看到打印垃圾回收的日志信息



可以看到,新生代的复制清除屏蔽比较高,mark-sweep清除也是持续在做

在具体的开发实战中,有几种情况需要注意。下面讲几个例子,清楚了会对开发中对于内理解存的回收有很大帮忙

var foo =function(){
var local='helloworld';
}
foo这个函数每次被调用时都会创建对应的作用域。函数执行结束后,该作用域就会销毁。同时local也会随着作用域的销毁而销毁。
在上面这个例子中,对象这小,将会分配在新生代中的From中,在施放后,local失效,引用的对象将会在下次回收时被施放。

node作用域链的引用是往上的,即从当前作用域开始找,找不到就继续往上。

var foo =function(){
var local='local var ';
var bar =function(){
var local='another var';
var baz = function(){
console.log(local);
};
baz();
};
bar();
};
foo();
在上面的例子中,baz在打印local变量时,会在baz范围内找,如果没有,就会往上一级,找到bar里面的local,打印出another var,如果删除another var ,则会打印local var的值。
理解了这块,我们应该明白,node对于变量定义的一些规范了,即最全局的变量是大家都可以引用的,即定义在global变量。因为它会常驻内存,不会被回收。所以,我们对于全局变量的定义也得慎重。

闭包的使用

nodejs可以传递方法以及做方法做为返回值传递,所以,在方法中定义的变量也没法被回收,这点在定义闭包时也得小心使用。不要定义过大的闭包函数

参考:
1.自制编程语言
2.深入浅出nodejs
3.深入理解JAVA虚拟机
4.WIKI:http://en.wikipedia.org/wiki/Mark-compact_algorithm
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: