JavaScript深入 — 垃圾回收
- 内存管理
- 可达性
- 内存回收机制
- 标记清除
- 标记整理
- 引用计数
- V8对于垃圾回收机制的优化
- 分代式垃圾回收
- 新生代内存回收
- 老生代内存回收
内存管理
在JavaScript编程中,内存管理大概分成三个步骤,也是内存的声明周期:
- 分配你所需系统内存的空间
- 使用分配到的内存进行读写操作
- 不需要使用内存时,将空间进行释放和归还
与其它手动管理内存的语言不一样的是,在JavaScript中,当我们创建变量时,系统会给对象进行自动分配对应的内存空间以及闲置资源回收
对于基础数据类型和引用数据类型,符合直觉的内存分配机制如下:
- 简单数据类型内存保存在固定的栈空间中,可直接通过值进行访问
- 引用数据类型的值大小不固定,其引用地址保存在栈空间、引用所指向的值保存在堆空间中,需要通过引用进行访问
栈内存中的基本数据类型,可以直接通过操作系统进行处理,而堆内存中的引用数据类型的值大小不确定,因此需要JS的引擎通过垃圾回收机制进行处理。
可达性
JavaScript中内存管理的主要概念是可达性
简单来说,“可达性”值就是哪些以某种方式可访问或可用的值,它们被保证存储在内存中
🌰 一个简单的例子
// user 具有对象的引用
let user = {name: "John"
}
👆 这里箭头表示一个对象引用,全局变量 user
引用对象 {name:"John"}
,如果user
的值被覆盖,则引用丢失
user = null
👆 现在对象 {name:"John"}
变成了不可达的状态,没有办法访问它,垃圾回收器将丢弃其数据并释放内存
内存回收机制
垃圾回收算法:垃圾收集器按照固定的事件间隔,周期性的寻找哪些不再使用的变量,然后将其清除或释放内存。
在浏览器的发展历史上有两种解决策略:
- 标记清除
- 引用计数
标记清除
首先它会遍历堆内存上所有的对象,分别给它们打上标记,然后在代码执行过程结束之后,对所使用过的变量取消标记。在清除阶段再把具有标记的内存对象进行整体清除,从而释放内存空间。
大致过程如下:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记
- 然后从各个根对象开始遍历,把还在被上下文变量引用的变量标记去掉标记
- 清理所有带有标记的变量,销毁并回收它们所占用的内存空间
- 最后垃圾回收程序做一次内存清理
使用标记清除策略最重要的优点在于简单,无非是标记和不标记的差异。通过标记清除后,剩余的对象内存位置是不变的,也导致空闲内存空间是不连续的(如上图),这就造成了出现内存碎片的问题。对于标记清除产生的内存碎片,还是需要通过标记整理策略进行解决。
- 优点:简单
- 缺点:内存碎片化、分配速度慢
👇 图示标记清除的过程
👇 第一步标记根
👇 然后标记他们的引用以及子孙代的引用
👇 现在进程中不能访问的对象被认为是不可访问的,将被删除
标记整理
标记结束后,标记整理算法会将不需要清理的对象向内存的一端移动,最后清理掉边界的内存
引用计数
引用计数是一种不常见的垃圾回收策略,思路是对每个值都记录其的引用次数,具体如下
- 当变量进行声明并赋值后,值的引用数为1
- 当同一个值被赋值给另一个变量时,引用书+1
- 当保存该值引用的变量被其它值覆盖时,引用数-1
- 当该值的引用数为0时,表示无法再访问该值了,此时就可以放心的将其清除并回收内存
💥 但有循环引用问题如下
function problem(){let objectA = new Object();let objectB = new Object();objectA.somOtherObject = objectB;objectB.anotherObject = objectA;
}
如上例子中,objectA
和 objectB
通过各自的属性相互引用,意味着它们的引用数都是2,在标记清理策略下,objectA
和 objectB
虽然互相引用了对方,但没有其他引用指向这两个对象,它们将会被标记为不可达,并最终会被垃圾回收器清除。
V8对于垃圾回收机制的优化
大多数浏览器都是基于标记清除算法,V8对其进行了一些优化加工处理。
- 分代式垃圾回收
- 新生代内存回收
- 老生代内存回收
分代式垃圾回收
V8的垃圾回收策略主要基于分代式垃圾回收机制,V8中将堆内存分为新生代和老生代两区域,采用不同的策略管理垃圾回收
- 新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持 1~8M 的容量
- 老生代的对象为存活事件较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大。
新生代内存回收
在64位操作系统下分配为32MB,因为新生代中的变量存活时间短,不太容易产生太大的内存压力
通常通过Scavenge的算法进行垃圾回收,就是将新生代内存进行一份为二,正在被使用的内存空间称为使用区,而限制状态的内存空间称为空闲区
- 新加入的对象都会存放在使用区,当使用区快写满时就进行一次垃圾清理操作。
- 在开始进行垃圾回收时,新生代回收器会对使用区内的对象进行标记
- 标记完成后,需要对使用区内的活动对象拷贝到空闲区进行排序
- 而后进入垃圾清理阶段,将非活动对象占用的内存空间进行清理
- 最后对使用区和空闲区进行交换,使用区->空闲区,空闲区->使用区
老生代内存回收
对内存空间较大的不适合上述Scavenge算法,此时应使用标记清除和标记整理的策略进行老生代内存中的垃圾回收