对比js与v8垃圾回收机制

前面的话

js具有自动回收机制,它会定期的找出那些不在继续使用的变量,然后释放其内存;而c、c++语言必须手动释放内存,程序员负责内存管理。

回收方法

  • 标记清除

    标记清除是最常用的垃圾回收方式。 当变量进入执行环境时,就标记这个变量为"进入环境"。 当变量离开环境时,则将其标记为"离开环境"。被标记为离开环境的变量,在下一次垃圾回收启动时,就会被释放掉占用的空间。

    举个例子:

     var m = 0 ,n = 4;// 把 m, n,add() 标记为进入环境
     add(m ,n);// 把 a,b,c标记进入环境。
     console.log(n);// add() 函数执行完之后,将a,b,c标记为离开环境,等待垃圾回收
     function add(a, b) {
    	 a++;
    	 var c = a + b;
    	 return c;
     }
    
  • 引用计数

    跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。

    缺点:无法解决循环引用:

    function func() {
        let obj1 = {};
        let obj2 = {};
    
        obj1.a = obj2; // obj1 引用 obj2
        obj2.a = obj1; // obj2 引用 obj1
    }
    

    当函数func执行结束之后,返回值undefined,所以整个函数以及内部的变量都应该被回收,但是,引用计数方法,obj1和obj2的引用次数都不为0,所以他们不会被回收。

    要解决循环引用的问题,最好是在不使用它们的时候将它们手动设为空。上面的例子可以这样:

     obj1 = null;
     obj2 = null;
    

哪些会引起内存泄漏

内存泄漏:不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

  • 意外的全局变量:比如在函数内部,没用使用var声明的变量。
  • 被遗忘的计时器:计时器没有被清除
  • 闭包
  • 没有被清理的DOM元素引用

内存泄漏的识别方法

在新版本的Chrome中的performance中查看
在这里插入图片描述
图中Heap对应的部分就是内存在周期性的回落。

扫描二维码关注公众号,回复: 9032712 查看本文章

如何避免泄漏

  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
  • 避免死循环
  • 避免创建过多的对象

总之:不用了的东西要及时归还。

V8引擎回收机制

内存申请:

  • 64位系统:1.4GB
  • 32位系统:700MB

在node环境中可以通过:process.memoryUsage()来查看内存分配。
在这里插入图片描述

  • rss: 所有内存占用
  • heapTotal:v8引擎可以分配的最大堆内存
  • heapUsed:v8引擎分配使用的堆内存
  • external:v8管理c++对象绑定到JavaScript对象的内存

以上单位都是字节。

Node的整体架构

node

  • 第一层: Node标准库,由js编写,是我们使用过程中直接能调用的API。在源码中的lib目录下。比如常用的http模块

  • 第二层:Node bindings,这一层是js与底层c/c++能够沟通的关键,前者通过bindings调用后者,相互交换数据。

  • 第三层:是支撑Node运行的关键,由c/c++实现。

      1、v8是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说
      	它就是Node.js的发动机。
      2、libuv是专门为Node.js开发的一个封装库,提供跨平台,线程池、
        事件池、异步I/O能力,是Node.js如此强大的关键。
      3、C-ares提供了异步处理DNS相关的能力
      4、http_parser、 OpenSSL、zlib等提供http解析,ssl、数据压缩等其他能力
    
v8垃圾回收策略

我们不可能只用一种回收策略来解决问题,这样效率会很低。所以,v8采用了分代回收的策略,将内存分为两个带:新生代老生代

  • 新生代中的对象:为存活时间较短的对象
  • 老生代中的对象:为存活较长或常驻内存的对象
分代内存:
  • 32位中,新生代内存大小16MB,老生代内存大小700MB
  • 64位中,新生代内存大小32MB,老生代内存大小1.4GB
  • 新生代平均分为两块相等的内存空间,叫做semispace,每块内存大小8MB(32位)或者16MB(64位)

在这里插入图片描述

新生代

新生代采用Scavenge垃圾回收算法,在算法实现时主要采用Cheney算法。

Cheney算法将新生代的内存分为两块semispace,一块处于使用状态,称为From空间,一块使用闲置状态,称为To空间

绘图说明:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上图中,一直将From和To交换,就是为了让活跃对象始终在From中,To始终保持闲置。

这种算法的缺点是:只能使用堆内存的一半。

由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模的应用到所有的垃圾回收中。但我们可以看到,Scavenge非常适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰适合这个算法。

当对象经过多次复制任然存活,它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中。采用新的算法进行管理。

对象从新生代移到老生代的过程叫作晋升。

晋升的条件:

  • 如果一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中。
  • 当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
老生代

由于老生代中的对象存活时间长,使用上面的方法将不在适用。老生代将使用Mark-SweepMark-Compact相结合的方式进行垃圾回收。

1、Mark-Sweep:

Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。Mark-Sweep在标记阶段遍历堆内存中的所有对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象。

注意: js中的标记是标记所有的变量,清除掉被标记为离开状态的变量;而老生代中的标记使标记存活的变量,清除没有被标记的变量。
在这里插入图片描述
在这里插入图片描述
缺点: 从图中可以看出,在进行一次清除回收之后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题。如果出现需要分配一个大内存的情况,由于剩余的碎片空间不足以完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。

2、Mark-Compact

为了解决Mark-Sweep的内存碎片问题,Mark-Compact就被提出来了。

Mark-Compact是标记整理的意思,会将标记存活对象以后,将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的内存。

在这里插入图片描述
在这里插入图片描述
3、两者结合

在V8的回收策略中,Mark-Sweep和Mark-Conpact两者是结合使用的。由于Mark-Conpact需要移动对象,所以它的执行速度不可能很快,在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时,才使用Mark-Compact。

参考文章:

发布了244 篇原创文章 · 获赞 181 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41257129/article/details/104199725