前言
我们知道,JavaScript 中的变量分为两种:基本类型和引用类型。基本类型的值存储在栈内存,引用类型的值存储于栈内存和堆内存,栈内存中保存的是堆内存地址,地址指向堆内存中保存着的具体的值。栈内存中的变量值使用完之后会自动出栈被立即回收,但是堆内存中的值则需要某种策略或手动回收。
JavaScript 自带一套内存管理引擎来进行内存分配以及垃圾回收,那么为了更好的开发和维护高性能的 JavaScript 代码,我们需要对垃圾回收机制进行一些了解。
下面我们从以下几个方面来了解垃圾回收机制
- 什么是垃圾回收
- 垃圾是怎么产生的
- 垃圾回收机制
什么是垃圾回收
垃圾回收机制的原理即找到不再使用的变量,释放其内存,因此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这已操作。
垃圾是怎么产生的
当一个对象没有任何变量或属性对它进行引用时,意味着我们无法在操作该对象了,这种对象就是所谓的”垃圾”,此时就需要进行垃圾回收,如果不进行清理,内存占用越来越高,就会影响性能,甚至会导致进程崩溃。
比如:
1 | let obj = [ 'a', 'b', 'c' ] |
在 JavaScript 中,引用类型的数据保留在堆内存中,栈内存中会保留一个地址,这个地址即为堆内存中保存的值。
上面先声明了一个变量,为一个数组,之后,我们把这个变量重新赋值,那么之前的引用关系就没有了,此时,之前的那个数组就会失去引用关系,也就是我们无法在操作它了,它就变成了 “垃圾”,等待被回收。
垃圾回收机制
最常见的垃圾回收机制有两种:
- 标记清除
- 引用计数
标记清除(Mark-Sweep)
标记清除,简单来说,就是使用某种方法,将不再使用的变量,也就是无用变量标记出来,等待垃圾收集器回收,释放内存。
那么,垃圾收集器是如何标记无用变量呢,这里,我们先了解一个概念——可达性。
可达性
可达性,指的是变量从”根”出发,经过一层或多层可以被访问到。即一个变量从”根”出发,可以被访问到,那么它就是”可达”的,垃圾回收器将这些”可达”的变量视为有用变量,反之则视为无用变量,无用变量会被打上标记,便于之后回收。
在 JavaScript 中,有一些基本的固有可达值,如:
- 本地函数的局部变量和参数
- 当前嵌套调用链上的其他函数的变量和参数
- 全局变量
- 一些其他的内部的值
举一个简单的例子:
1 | let user = { |
这里是一个全局变量 user 引用了对象,当我们把 user 重写,那么这个引用就没有了,没有了引用就变成了不可达的,就会被回收
了解了可达性,我们就来看下垃圾回收器是如何进行标记的吧,各个浏览器的具体实现不太相同,其运行机制如下:
- 变量进入上下文,会被加上标记,证明其存在于该上下文
- 将所有在上下文的变量以及上下文中被访问引用的变量标记去掉,表明这些变量活跃
- 之后再被加上标记的变量为准备删除的变量,因为上下文中已访问不到它们了
- 执行内存清理,销毁所有带标记的非活跃值并收回之前被占用的内存
标记清除有一个缺点就是-内存碎片,因为标记清除,在清除之后,剩余内存的位置是不变的,所以会导致空闲的内存空间是不连续的,大小不同的碎片。
想要解决这个缺点,就需要标记整理,它会在标记结束后,将不需要清理的对象移至内存的一端,最后清理掉边界的内存。
引用计数(Reference Counting)
引用计数,把”对象是否不再需要”简化定义为”对象有没有其他对象引用到它”,即该对象没有被任何对象或变量引用(零引用),就会被垃圾回收机制回收,其运行机制如下:
- 当声明了一个变量,并赋予它一个引用的值时,该值的引用次数 +1
- 当同一个值被赋值给另一个变量的时候,引用次数 +1
- 当该变量被另一个值覆盖的时候,引用次数 -1
- 当引用次数为 0 的时候,就会被内存回收
引用计数有一个最大的问题,就是循环引用,如下:
1 | function test () { |
如上,两个都互相引用了,引用计数不为 0 ,所以无法被回收,这样就会造成内存泄漏。