js核心系列(三) —— 垃圾回收机制

445 阅读5分钟

js核心系列流程图.png

什么是垃圾回收

JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时自动释放。释放的过程称为垃圾回收。JavaScript垃圾收集器自动运行,会跟踪内存位置,并确定哪些内存位置是空闲的(即可以安全地重用)

内存生命周期

不管什么程序语言,内存生命周期基本是一致的

  1. 分配你所需要的内存
  2. 使用分配到的的内存(读、写)
  3. 不需要时将其释放和归还

垃圾回收算法

引用计数

定义

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

var person = {
  jimmy: {
    age:18
  }
};

// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 person
// 很显然,没有一个可以被垃圾收集


var person2 = person;

// person2 变量是第二个对“这个对象”({
//  jimmy: {
//    age:18
//  }
// })的引用

person = 1;  // 现在,“这个对象”只有一个 person2 变量的引用了,“这个对象”的原始引用person已经没有

var jimmyObj = person2.jimmy; // 引用“这个对象”的 jimmy 属性
                              // 现在,“这个对象”有两个引用了,一个是 person2,一个是 jimmyObj

person2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
                // 但是它的属性 jimmy 的对象还在被 jimmyObj 引用,所以还不能回收

jimmyObj = null; // jimmyObj属性的那个对象现在也是零引用了
                 // 它可以被垃圾回收了
                 

我们再来一个更具体一点的例子

当一个对象被创造初始化并赋值之后,该变量计数就设置为1

var a = new Object() // 计数变量 = 1

每当有一个地方引用它时,计数器的值就加1

var a = new Object() // 计数变量 = 1

var b = a   // 计数变量 + 1 = 2

当引用失效时,计数器的值就减1

var a = new Object() // 计数变量 = 1

var b = a   // 计数变量 + 1 = 2

var c = a   // 计数变量 + 1 = 3

a = null    // 引用失效,计数变量 -1 = 2

b = {}      // 引用失效,计数变量 -1 = 1

当该对象的计数值为0时,就表示失去了所有的引用,该对象就成为了垃圾。

var a = new Object() // 计数变量 = 1

var b = a   // 计数变量 + 1 = 2

var c = a   // 计数变量 + 1 = 3

a = null    // 引用失效,计数变量 -1 = 2

b = {}      // 引用失效,计数变量 -1 = 1

c = null    // 引用失效,计数变量 -1 = 0

缺陷:循环引用

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

  var obj = {};
  var obj2 = {};
  obj.a = obj2; // obj 引用 obj2
  obj2.a = obj; // obj2 引用 obj

IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时发生内存泄漏

标记 - 清除算法

定义

这个算法把“对象是否不再需要”简化定义为“对象是否可以访问”。垃圾回收机制从根目录开始遍历这些对象,并将它们标记为已访问。无法从根访问的对象是未标记的,被认为是垃圾(为这些对象保留的内存稍后将被释放)。

可访问对象指的是那些可访问并存储在内存中的对象。

示例

let fruit = {
  name"Orange",
  weight130,
};
let orange = fruit;

“fruit”和‘orange’都包含对 对象的引用,如下图:

1682064385870.jpg

这是 我们 将fruit = null,fruit不再包含对象的引用。但是,它仍然可以从orange访问,不能作为垃圾处理。

1682064504026.jpg

我们将 orange = null,orange不再包含对象的引用。该对象现在是不可访问的,它所占用的内存将被声明为安全的,会打上一个标记,可由垃圾收集器重用。

这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

从 2012 年起,所有现代浏览器都使用了标记清除的垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记清除算法的改进,并没有改进标记清除算法本身和它对“对象是否不再需要”的简化定义(源自MDN)

内存泄露

JavaScript 内存泄漏指的是在使用 JavaScript 开发过程中,不小心创建了一些对象,这些对象没有被正确地清除或销毁,导致它们一直在内存中存在,直到程序结束,占用宝贵的内存资源。

有那些可能会造成内存泄漏的操作呢?

  • 定时器

我们创建了一个循环定时器,而忘记清除它,这时会造成内存泄露

  • 事件监听 比如window.addEventListener,而没有removeEventListener

  • 全局变量 这个不会被 GC,要注意全局变量的使用

  • 未清理的Console输出

写代码的过程中,肯定避免不了一些输出,在一些小团队中可能项目上线也不清理这些 console,殊不知这些 console 也是隐患,同时也是容易被忽略的,我们之所以在控制台能看到数据输出,是因为浏览器保存了我们输出对象的信息数据引用,也正是因此未清理的 console 如果输出了对象也会造成内存泄漏。

所以,开发环境下我们可以使用控制台输出来便于我们调试,但是在生产环境下,一定要及时清理掉输出。

那为啥 console.log 会导致内存泄漏呢?

因为控制台打印的对象,你是不是有可能展开看?那如果这个对象在内存中没有了,是不是就看不到了?

所以有这个引用在,浏览器不会把你打印的对象的内存释放掉。

参考文章

内存管理