JS 垃圾回收机制
shell
# 简介
JS 使用自动垃圾回收机制来管理内存,开发者不需要手动分配和释放内存。
垃圾回收的算法有二:最早的引用计数 和 现代浏览器的标记清除
引用计数有个致命缺点就是循环引用误区/陷阱
shell
# 其一:全局不可回收
不管是引用计数,还是标记清除算法;
全局环境下用 var / let / const 声明的变量;
只要不手动赋值为 null 断开引用,永远都不会被垃圾回收。
# 其二:基础类型不参与垃圾回收
垃圾回收(引用计数、标记清除)只管堆内存里的对象,不管栈!
ndefined、Symbol、BigInt)不参与引用计数!也不参与标记清除垃圾回收!
那基础类型怎么回收呢?全局基础类型永不回收,函数中的基础类型,在函数执行完毕,函数执行上下文出栈销毁,基础类型存储在栈中,所以直接销毁
那函数中的引用类型怎么回收?函数中的引用类型初始的时候标记为活的,在函数执行完毕,函数执行上下文出栈销毁,引用类型的指针存储在栈中,所以指针销毁,堆中的数据编程不可达,被GC垃圾回收
# 根本原因
垃圾回收,只是回收堆中的数据
栈中的数据有自己的声明周期,无需垃圾回收:全局永存、函数内随着函数执行完毕自行销毁
引用计数的计数 和 标记清除的标记都是在堆中对象的内部做的,再次说明垃圾回收只是关心堆中数据
# 注意
全局作用域的数据永存,函数作用域随栈销毁,是编程需要,不是垃圾回收不了引用计数算法
早期浏览器使用的算法,现在已被淘汰
shell
每个对象维护一个引用计数,当引用数为0时回收(实时回收,不分阶段)
首先全局对象:初始化被引用计数一次,每再被引用一次计数加一,没被赋值为 null 计数减一,当计数为 0 时销毁堆中数据,
其次函数对象:初始化被引用计数一次,每再被引用一次计数加一,没被赋值为 null 计数减一,当计数为 0 时销毁堆中数据,除此之外
函数在执行完毕后,函数执行上下文出栈销毁,栈指针直接消失断开引用,每断开一次堆对象引用计数减法一,直至为 0 时销毁堆中数据
# 致命缺陷
这种引用计数机制造成了循环应用的缺陷(两个对象互相引用)标记清除算法
现代 JS 引擎(V8)全部使用标记清除,不用引用计数。
shell
标记清除分为两个阶段
标记阶段:从根对象(全局对象、当前函数调用栈等)开始,标记所有可达对象
清除阶段:遍历堆内存,清除未被标记的对象,保留被标记的对象
换句话说:标记阶段,标记全局作用域和函数作用域中的所有对象,清除阶段,首先全局对象会永远保留,除非赋值为 null,否则不会销毁
其次函数作用域内的对象在函数执行完毕,函数执行上下文出栈销毁,引用类型的指针被销毁,标记销毁,导致在清除阶段没有标记,存储在堆中的数据被回收掉
# 注意
所谓的根对象:全局是根、函数内部也是根V8 引擎的垃圾回收算法
现代 JavaScript 引擎(如 V8)采用更复杂的策略
shell
1. 分代回收 (Generational Collection)
将内存分为新生代和老生代
新生代:存放存活时间短的对象,使用 Scavenge 算法(复制算法)
老生代:存放存活时间长的对象,使用标记-清除或标记-整理算法
2. 增量标记 (Incremental Marking)
将标记过程分成多个小步骤,避免长时间阻塞主线程
3. 空闲时间回收 (Idle-time Collection)
利用浏览器空闲时间进行垃圾回收