JS内存管理和垃圾回收

148 阅读5分钟

JS内存生命周期

  1. 内存分配:申明变量、函数、对象的时候,系统自动为他们分配内存
  2. 内存使用:读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,垃圾回收机制自动回收不再使用的内存

栈内存&堆内存

栈stack为自动分配的内存空间,它由系统自动释放

堆heap是动态分配的内存,大小不定,也不会自动释放

栈内存运行效率比堆内存高,空间相对堆内存来说较小。堆内存反之。

JS中:

  • 对于原始类型的值而言,其地址和具体内容都存在于栈内存中
  • 基于引用类型的值,其地址存在于栈内存,其具体内容存在于堆内存中

所以将构造简单的原始类型值放在栈内存中,将构造复杂的引用类型值放在堆中而不影响栈的效率。

wuch886.gitbooks.io/front-end-h…

垃圾收集

找到“哪些被分配的内存确实已经不再需要了” ,然后释放

以下两种方式都是对这一概念进行降级

先探讨以下什么是引用

引用

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

 在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

引用计数法

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”降级为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

缺点

无法处理循环引用

在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

标记清除法

这个算法把“对象是否不再需要”这个定义降级为“对象是否可以触及到”。

 这个算法假定设置一个叫做根(root)的对象(根对象在浏览器中是 window 对象,在 NodeJS 中是 global 对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

 从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。 但反之未必成立。

工作流程: 

  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。 
  2. 从根部出发将能触及到的对象的标记清除。 
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

循环引用的问题解决了。在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记-清除算法性能和实现的改进,并不是对算法本身。

缺点

有时候手动释放内存更方便,但从19年起我们就没办法手动释放内存了,所以这时候只能把该对象设置成unreachable。有点麻烦。

内存泄露

本质上讲,内存泄漏就是由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费 

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。 否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

案例

juejin.cn/post/684490…

  1. 意外的全局变量。声明到window上,可以触及,无法回收。

  2. 被遗忘的定时器和回调函数。定时器或事件回调引用外部对象,没清空会导致外部对象不会被回收。

  3. 闭包。滥用闭包会造成内存泄漏,比如闭包循环引用(因为bar一直存在并且指向foo,导致foo的内存没有被回收)

    const heapdump = require('heapdump');
    heapdump.writeSnapshot('start.heapsnapshot'); // 记录应用开始时的内存dump
    let foo = null;
    function outer() {
        let bar = foo;
        function unused() { // 未使用到的函数
            console.log(`bar is ${bar}`);
        }
    
        foo = { // 给foo变量重新赋值
            bigData: new Array(100000).join("this_is_a_big_data"), // 如果这个对象携带的数据非常大,将会造成非常大的内存泄漏
            inner: function() {
                console.log(`inner method run`);
            }
        }
    }
    for(let i = 0; i < 1000; i++) {
        outer();
    }
    heapdump.writeSnapshot('end.heapsnapshot'); // 记录应用结束时的内存dump
    

    )

  4. DOM引用。即使我们对于元素进行了移除,但是仍然有对该元素的引用,依然无法对其进行内存回收。

如何避免

记住一个原则:不用的东西,及时归还。

  1. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。
  2. 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
  3. 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

Ref

www.helloworld.net/p/374905506…

juejin.cn/post/684490…

developer.mozilla.org/en-US/docs/…