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



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虚拟机
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值