这次要说的是我们开发过程中基本很少用到的东西—JavaScript的垃圾回收机制。作为博主在JavaScript中为数不多可以写到简历的东西,那必须要给大伙整个明明白白的。作为动态弱类型语言,JavaScript的垃圾回收是属于自动执行,可以帮助我们开发少操心很多内存问题,当然,人无完人,垃圾回收机制也是一样的,那么我们来开始慢慢的了解它的发展。
在开始垃圾回收器学习之前,我觉得大家有必要知道javaScript的数据是怎么存储的。
栈空间与堆空间
其实我们都知道,对于JS来说,数据的类型主要分为两个大类,分别为基础数据类型以及引用数据类型。为了方便读者们在阅读时的观感不受影响,这里简单的列出这个两个分类。
基础数据类型:
- String
- Number
- Boolean
- Null
- Undefined
- Bigint
- Symbol
引用数据类型:
- Object
- Array
要区分这两种类型其实很简单,使用typeof即可,对于基础数据类型来说,除了null这个早期的bug以外,这几种数据类型都会出现对应的名称,看看下面代码:
1 | |
至于为什么null为什么会是object,早期JS的数据以32字节来储存,由标志位(1~3个字节)和数值组成。这里有五种标志位:
- 000:对象,数据是对象的应用。
- 1:整型,数据是31位带符号整数。
- 010:双精度类型,数据是双精度数字。
- 100:字符串,数据是字符串。
- 110:布尔类型,数据是布尔值。
对于特殊值则是:
- undefined(JSVAL_VOID)是-2^30(一个超出整数范围的数字)
- null(JSVAL_NULL)是机器代码的空指针,一个对象类型的引用,值是零。
对于一个值全零的null来说,自然在判断的时候,会变成object.
上面是使用JS中的一个内置方法来很明显的判断出这两种类型,从储存空间来说的来判断的话,对应的就是两种不能的空间,就是栈空间以及是堆空间。在开始说这两种空间之前,有必要了解一下JavaScript中的内存空间,看下图:
而对于JavaScript来说,基础类型都是存储在栈空间,引用类型则存储在堆空间。先看代码:
1 | |
我们知道,在js执行过程中会先编译,并创建执行上下文,然后才开始执行代码。那么我们看看上述代码执行到第三行的状态图:

但是执行到第四行的时候,JavaScript判断到这个变量c是一个引用类型,那么引用类型的数据就会存到堆空间里面,如下图若所示:

从上图你可以清晰地观察到,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的,相当于多了一道转手流程。那么为什么JavaScript要设置栈空间以及堆空间呢?不能所有的数据都存储在栈空间吗?答案肯定是不可以的,参考下图。

所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。
解释了程序在执行过程中为什么需要堆和栈两种数据结构后,我们还是回到示例代码那里,看看它最后一步将变量 c 赋值给变量 d 是怎么执行的?
在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。
所以d=c的操作就是把 c 的引用地址赋值给 d,你可以参考下图:

从图中你可以看到,变量 c 和变量 d 都指向了同一个堆中的对象,通过 c 修改 name 的值,变量 d 的值也跟着改变,归根结底它们是同一个对象。
那么至此,对于JavaScript的数据存储就结束了。
什么是垃圾回收
看完JavaScript的数据存储,其实垃圾回收就很明显了,就是回收无用变量的内存空间,那么垃圾数据是怎么产生的呢?看下面代码:
1 | |
函数编译并创建执行上下文,在函数foo活动对象中有变量a以及变量b,在函数foo执行完毕后,执行上下文指针就会指向全局执行上下文,这个时候对于JavaScript来说函数foo就属于垃圾数据,会将foo函数内的活动对象摧毁,并回收其占用的内存。
除此以外,还有下面一种情况:
1 | |
在全局对象window中新增一个test属性赋值为对象,然后在其新增属性a赋值为一个长度为2的空数组。从JavaScript数据存储我们知道,这时候不论是对象window,test,a都是存在堆空间的。这时候执行下面代码:
1 | |
可以看到,我们将a属性的值改变了,准确的来说,我们改变了属性a的值地址,那么原来的值就会变成没有被使用的状态,那么这个内存空间就可以看作是垃圾数据空间,JavaScript就会自动回收到这样的数据。那么JavaScript到底是怎么找到这些垃圾数据,然后自己回收的呢?
垃圾回收的进化
在JS的进化中,用到过两种主要的标记策略:标记清理和引用计数。
标记清理
当变量进入上下文,比如在函数内容部声明一个变量时,这个变量会被加上存在于上下文中的标记。而不在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。当然,怎么样去标记是不重要的,关键的是什么时候去回收这个变量占用的内存。以一个非常经典的题目来作为示例—闭包,看下面代码:
1 | |
我们知道,在函数活动对象里的代码执行完以后,那么垃圾回收就会触发作用,回收到这个活动对象里所有占用的内存,也就是说,按照这个思路来操作的话,那么我们每次触发垃圾回收的时机就可以认为是每一个函数执行完以后,就会触发一下垃圾回收,然后回收到被占用的内存。,如果只看普通的方法好像并没有问题。 如果是闭包的话,再按照这个逻辑就不对了,如上述的函数foo,那么变量a是不可能被外部所能访问的,但是实际上是可以被访问的。实际上,垃圾回收也是在全局代码运行完以后,可以有一个池子,记录到所有已经去掉标记的变量,然后统一进行回收(注意,这是方式的一种),那么对于闭包这样的情况就可以完美执行了。
引用计数
这个方式其实很简单,就是统一每个变量被使用的次数,变量被声明并复制的话,那么计数就会加一,如果是复制到其他值,那么相应的就会计数减一。(针对引用类型)当计数为0的时候,说明这个变量就没有价值了,那么就会相应的进行回收内存,但是很快就遇到了一个严重的问题:循环引用。就是A对象对B对象有引用,而B对象对A对象也有相应的引用。如下:
1 | |
在引用计数策略下,他们互相的引用计数是2,是两个永远不会被清理的变量。但是在标记清理策略下,代码运行完后依然是可以清理到。
垃圾回收的性能提升
其实在性能提升这方面能提及的东西并不到,主要的还是减少垃圾回收的频率即可,最简单的方式就是建议创建变量,如下:
1 | |
在上述函数中,变量result的声明周期其实很短,就在函数foo中使用。频繁使用foo函数会导致这个声明周期很短的变量不断重复占用内存然后被清理的这个过程,这可能会导致垃圾回收频繁的触发,这其实是会影响到浏览器运行时的效果。解决办法其实很简单,将这个创建过程减少,可以变成下面:
1 | |
当然,在这个变量没有用的时候,也可以相对应的设置为null,虽然不会立即触发垃圾回收,但是也可以在回收的时候减少这个变量的引用。
总结
以上就是本篇文章的内容,相对来说没有那么硬核,所以打算后续会更新V8中的垃圾回收机制。