图解JavaScript的垃圾回收机制

1,104 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与 「掘力星计划」,赢取创作大礼包,挑战创作激励金


前言

💥 排雷,本文主要针对初学GC的读者

💥 排雷,如果觉得前面的文字太多,可以直接跳到【示例】


什么是垃圾回收?

根据 Wiki 的定义,垃圾回收(英语:Garbage Collection,缩写为GC)是指一种自动的内存管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。

当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。垃圾回收最早起源于LISP语言。[1][2]目前许多语言如SmalltalkJavaC#GoD语言都支持垃圾回收器。

在C与C++等语言中,开发人员可以直接控制内存的申请和回收。但是在Java、C#、JavaScript语言中,变量的内存空间的申请和释放都由程序自己处理,开发人员不需要关心。也就是说Javascript具有自动垃圾回收机制

垃圾回收的好处?

如果没有GC,程序员必须自己手动进行内存管理,必须清楚地确保必要的内存空间,释放不要的内存空间。程序员在手动进行内存管理时,申请内存尚不存在什么问题,但在释放不要的内存空间时,就必须一个不漏地释放。这非常地麻烦。

如果忘记释放内存空间,该内存空间就会发生内存泄露,即无法被使用,但它又会持续存在下去。如果将发生内存泄露的程序放着不管,总有一刻内存会被占满,甚至还可能导致系统崩溃。——引自《垃圾回收的算法与实现》

理解JS里面的垃圾回收

JavaScript 中主要的内存管理概念是 可达性,如果可以从任何一个已经定义的变量开始,直接或者通过其他对象的引用来访问到某个对象,则该对象是可达的。

在 JavaScript 引擎中有一个被称作垃圾回收器的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。

示例1

如下,定义了1个全局变量 "user" ,它引用了对象 {name:"John"}

// user 具有对这个对象的引用
let user = {
  name: "John"
};

如果重写user 的值

user = null;

这个时候,对象 {name:"John"}不再被引用,垃圾回收器会认为它是垃圾数据并进行回收,然后释放内存。

image-20211023103847831

示例2

现在在定义user对象的同时,在将它的引用复制给admin

let user = {
  name: "John"
};

let admin = user;  

在之前的深浅拷贝里有说过,对象直接用等号复制,复制的是它的引用👉地址

这个时候如果同样对user 的值进行重写,对象 {name:"John"}不会被回收,因为它还在被admin引用

user = null;

如果是同时对useradmin 重写,才会被回收

user = null;
admin = null;

image-20211023105008750

示例3

一个更复杂的例子,假如有这样一个”家庭“,marry 函数通过让两个对象相互引用使它们“结婚”了,并返回了一个包含这两个对象的新对象。

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

这个时候产生的内存结构,当前所有对象都是可达的

image-20211023110621872

如果删除以下这两个引用,John 现在是不可达的,并且将被从内存中删除,同时 John 的所有数据也将变得不可达。

delete family.father;
delete family.mother.husband;

image-20211023111746234

垃圾回收之后,当前结构就变成了

image-20211023111834875

注:上述情况只删除其中一个是没有用的,因为这个时候所有对象还是可达的。

image-20211023111412347

示例3.1

和上面的示例相同

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

但现在我们直接把family 重写,会是什么样的情况?

family = null;

内存的内部状态将变成

image-20211023114622076

这个时候,虽然John 和 Ann 仍然连着,都有传入的引用,但是它没有了外部对其的引用,这个时候它变成了一座“孤岛”,同样会从内存中被删除。

这个时候我们就需要提到一个“”的概念:
(root)这个词的意思是“根基”“根底”。在GC的世界里,根是指向对象的指针的“起点”部分。可达的标准是一定从上出发找得到的,JavaScript中的可以理解为是全局变量对象

几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。

垃圾回收内部算法

javascript中最常用的垃圾回收方式是 “mark-and-sweep”即标记清除

示例

假如当前有一个这样的对象结构

image-20211023120416382

根据标记清除算法,第一步会先标记所有的

image-20211023120535034

接着标记这些的引用

image-20211023120625022

……如果还有引用,继续标记

image-20211023120711625

全部标记完之后,没有被标记到的,就会被作为垃圾回收

image-20211023120811612

总结

  • 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
  • 当对象是可达状态时,它一定是存在于内存中的。
  • 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。

参考资料:

wiki Garbage collection (computer science)

javascript Garbage collection

V8 之旅:垃圾回收


🎨【点赞】【关注】不迷路,更多前端干货等你解锁

往期推荐

👉小程序template模板使用详解

👉 最全的CSS阴影总结

👉 喊你来学JavaScript的加减乘除啦!

👉简说JavaScript里的深浅拷贝!