JavaScript垃圾回收机制

不同于C和C++等语言由开发者负责跟踪内存使用,JavaScript通过自动内存管理实现内存分配和闲置资源回收。释放不再使用的变量占用的内存称为垃圾回收。无论是什么程序语言,内存的生命周期基本是一致的:首先是分配所需内存,接着使用分配到的内存(读、写),最后在不需要时将其释放。

内存分配

值的初始化

定义变量时完成内存分配:

      
        let n = 123; // 给数值变量分配内存

        function f(a){
          return a + 1;
        } // 给函数(可调用的对象)分配内存
      
    

函数调用

有的函数调用结果是分配对象内存:

      
        let d = new Date(); // 分配一个Date对象
    
        let e = document.createElement('div'); // 分配一个DOM元素
      
    

垃圾回收

主要的垃圾回收策略有标记清理和引用计数。

标记清理

标记清理的核心概念是可达性(Reachability),即“可达”值是那些以某种方式可访问或可用的值。它们一定存储在内存中。常见的可达值包括:

这些值被称作根(roots)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

示例:

      
        let user = {
          name: "John"
        }
      
    

在上例中,全局变量user引用了对象{name: "John"}。如果user的值被重写了,这个引用就没了:

      
        user = null
      
    

现在对象{name: "John"}变成不可达的了,因为没有引用,就无法访问到它,因此垃圾回收器会将其回收,并释放被其占用的内存。

引用计数

引用计数是一种不常用的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

示例:代码可视化示范

      
        var o = {
          a: {
            b:2
          } // 对象乙
        }; // 对象甲
        // 乙作为甲的属性被引用,甲被分配给变量o
        
        var o2 = o; // o2变量是甲的引用
        
        o = 1;      // 现在,甲只有一个变量o2的引用,原始引用o已经没有
        
        var oa = o2.a; // 变量oa引用乙
        
        o2 = "yo"; // 现在甲已经零引用了,可以回收,但是它的属性a引用的对象乙还被oa引用,无法回收
        
        oa = null; // 乙现在也是零引用了,它可以被垃圾回收了
      
    

引用计数的一个问题是循环引用,即对象A 有一个指针指向对象B,而对象B 也引用了对象A。比如:

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

在这种情况下,oo2在函数结束后还会存在,因为它们的引用数不为0。如果函数多此调用,则会导致大量内存永远无法释放。

References

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management

https://zh.javascript.info/garbage-collection

马特·弗里斯比. JavaScript高级程序设计:第4版,P95-96.