JS 内存泄漏 及 垃圾回收机制

内存泄漏

程序的运行需要 内存,只要程序提出要求,操作系统或者运行是就必须供给内存。对于持续运行的服务进程,必须及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。而不再用到的内存,没有及时释放,就叫做内存泄漏

常见内存泄露的原因:

  • 1、全局变量引起的内存泄露

    • 1)、bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。

      function foo() {
          bar = "没有被声明的变量";
      }
      
    • 2)、this指针造成的全局变量

      function foo() {
          this.variable = "this指向了window";
      }
      // foo 调用自己,this 指向了全局对象(window)
      foo();
      
  • 2、闭包引起的内存泄露

    function foo() {
        var arr = [];
        for (var i = 0; i < 4; i++) {
            arr[i] = function () {
                return i;
            }
        }
        return arr;
    }
    

    当外部函数运行结束甚至销毁时,该变量 i 的值仍保存在内存中

  • 3、被遗忘的定时器或回调函数

    var someResource = getData();
    setInterval(function() {
        var node = document.getElementById('Node');
        if(node) {
            // 处理 node 和 someResource
            node.innerHTML = JSON.stringify(someResource));
        }
    }, 1000);
    

    如果id为Node的元素从DOM中移除,该定时器仍会存在,并没有进行clear,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。

  • 4、dom清空或删除时,事件未清除导致内存泄漏

    var elements={
        button: document.getElementById("button"),
        image: document.getElementById("image"),
        text: document.getElementById("text")
    };
    function doStuff(){
        image.src="http://some.url/image";
        button.click():
        console.log(text.innerHTML)
    }
    function removeButton(){
        document.body.removeChild(document.getElementById('button'))
    }
    
  • 5、循环引用带来的内存泄露

    function fn(){
      var a={};
      var b={};
      a.pro=b;
      b.pro=a;
    }
    fn();
    

垃圾回收机制

1、垃圾的产生:

JavaScript 的引用数据类型是保存在 堆内存 中的,然后在 栈内存 中保存一个对堆内存中实际对象的引用,所以,JavaScript 中对引用数据类型的操作都是操作对象的引用而不是实际的对象。可以简单理解为,栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的

比如:首先声明了一个变量,它指向一个对象,建立了一个引用关系。接着把这个变量重新赋值了一个数组对象,也就变成了该变量引用了一个数组,那么之前的对象引用关系就没有了。

没有了引用关系,也就是无用的对象,如果这种无用的对象很多内存会不够,所以就需要被清理(回收)

2、GC: 即 Garbage Collection 垃圾回收器 。程序工作过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而 GC 就是负责回收垃圾的,会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收

因此,回收其实就是发现这些不可达的对象(垃圾)它并给予清理, JavaScript 垃圾回收机制的原理也就是 定期找出那些不再用到的内存(变量),然后释放其内存


垃圾回收有主要两种方法:标记清除法引用计数法

1)、标记清除法:

这是最常见的垃圾回收方式,目前在 JavaScript引擎 里这种算法是 最常用的,到目前为止的大多数浏览器 的 JavaScript引擎 都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同浏览器的 JavaScript引擎 在运行垃圾回收的频率上有所差异。

当变量进入环境时,就标记这个变量为”进入环境“,从逻辑上讲,永远不能释放进入环境的变量所占的内存,永远不能释放进入环境变量所占用的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。

垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。
在这里插入图片描述

每一个标记算法策略都不同,过程例如:

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  • 然后从各个根对象开始遍历,把不是垃圾的节点改成1
  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

优点:

  • 实现比较简单,标记与不标记两种情况实现容易
  • 能够回收循环引用的对象

缺点:

  • 在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,导致回收空间有可能不能被重新分配

扩展——标记整理法 :

它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
在这里插入图片描述


2)、引用计数

早期的浏览器最常使用的垃圾回收方法叫做"引用计数"(reference counting):语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的 引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

基本策略:

  • 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
  • 如果同一个值又被赋给另一个变量,那么引用数加 1
  • 如果该变量的值被其他的值覆盖了,则引用次数减 1
  • 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
var a = new Object(); // 此时'这个对象'的引用计数为1(a在引用)
var b = a; // ‘这个对象’的引用计数是2(a,b)
a = null; // reference_count = 1
b = null; // reference_count = 0 
// 下一步 GC来回收'这个对象'了

但是引用计数有个最大的问题: 循环引用

function func() {
    let obj1 = {};
    let obj2 = {};

    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

obj1和obj2通过各自的属性相互引用,所有它们的引用计数都不为零,这样就不会被垃圾回收机制回收,造成内存浪费

优点:

  • 引用计数为零时,发现 垃圾立即回收
  • 最大限度减少程序暂停
  • 不需要遍历堆里的活动以及非活动对象来清除,只需要引用时计数就可以

缺点:

  • 无法回收 循环引用 的对象
  • 需要计数器,空间开销比较大
  • 存在内存泄漏

V8 中对垃圾回收机制的优化:

3)、分代式垃圾回收

V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。

分代式把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接受检查。

在这里插入图片描述
新生代垃圾回收:将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区,一个是处于闲置状态的空间我们称之为 空闲区

  • 新加入的对象都会存放到使用区,当使用区快被写满时,就需要执行一次垃圾清理操作。
  • 当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序。
  • 随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区。
  • 当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理。

老生代垃圾回收:对于大多数占用空间大、存活时间长的对象会被分配到老生代里,因为老生代中的对象通常比较大,并且如果复制就会非常耗时,从而导致回收执行效率不高,所以老生代垃圾回收器来管理其垃圾回收执行,它的整个流程采用的就是上文所说的 标记清除算法

优点:

  • 大大提高了垃圾回收机制的效率

4)、并行回收

JavaScript 是一门 单线程的语言,它是运行在主线程上的,那在进行垃圾回收时就会阻塞 JavaScript 脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,我们把这种行为叫做 全停顿

并行回收:指的是垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作,新生代对象空间就采用并行策略,在执行垃圾回收的过程中,会启动了多个线程来负责新生代中的垃圾清理操作。

在这里插入图片描述


5)、增量标记与惰性清理

增量标记:

上面所说的并行策略虽然可以增加垃圾回收的效率,对于新生代垃圾回收器能够有很好的优化,但是其实它还是一种全停顿式的垃圾回收方式,对于老生代来说,它的内部存放的都是一些比较大的对象,对于这些大的对象 GC 时即便使用并行策略依然可能会消耗大量时间
在这里插入图片描述
增量就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记

懒性清理:

增量标记其实只是对活动对象和非活动对象进行标记,对于真正的清理释放内存 V8 采用的是惰性清理(Lazy Sweeping)

增量标记完成后,惰性清理就开始了。当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理过程稍微延迟一下,让 JavaScript 脚本代码先执行,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记

6)、并发回收

前面说并行回收依然会阻塞主线程,增量标记同样有增加了总暂停时间、降低应用程序吞吐量两个缺点

并发回收: 它指的是主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起(是优点、也是难点,所以实现较复杂)
在这里插入图片描述

总结:V8的GC优化

V8 的垃圾回收策略主要基于分代式垃圾回收机制,关于新生代垃圾回收器,使用并行回收可以很好的增加垃圾回收的效率,关于老生代收会收器,这几种策略都是融合使用的

  • 老生代主要使用并发标记,主线程在开始执行 JavaScript 时,辅助线程也同时执行标记操作(标记操作全都由辅助线程完成)
  • 标记完成之后,再执行并行清理操作(主线程在执行清理操作时,多个辅助线程也同时执行清理操作)
  • 同时,清理的任务会采用增量的方式分批在各个JavaScript 任务之间执行

以上内容均参考以下优秀文章:

JavaScript中的垃圾回收和内存泄漏

「硬核JS」你真的了解垃圾回收机制吗

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值