垃圾回收 GC
什么是垃圾回收机制
JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。这一释放的过程称为垃圾回收。
V8 引擎逐行执行 JavaScript 代码
的过程中,当遇到函数的情况时,会为其创建一个函数执行上下文(Context)
环境并添加到调用堆栈的栈顶,函数的作用域(handleScope)中包含了该函数中声明的所有变量,当该函数执行完毕
后,对应的执行上下文从栈顶弹出,函数的作用域会随之销毁
,其包含的所有变量
也会统一释放
并被自动回收
。
V8 引擎已经帮我们自动进行了内存的分配和管理,好让我们有更多的精力去专注于业务层面的复杂逻辑,这对于我们前端开发人员来说是一项福利,但是随之带来的问题也是显而易见的,那就是由于不用去手动管理内存,导致写代码的过程中不够严谨从而容易引发内存泄漏
为什么要进行垃圾回收
V8 引擎的内存限制
虽然 V8 引擎帮助我们实现了自动的垃圾回收管理,解放了我们勤劳的双手,但 V8 引擎中的内存使用也并不是无限制的。这个要回到 V8 引擎的设计之初,起初只是作为浏览器端 JavaScript 的执行环境,在浏览器端我们其实很少会遇到使用大量内存的场景,因此也就没有必要将最大内存设置得过高。但这只是一方面,其实还有另外两个主要的原因:
- JS 单线程机制
- 代码必须按顺序执行
- 在同一时间只能处理一个任务
- 垃圾回收机制。垃圾回收本身也是一件非常耗时的操作
基于以上两点,V8 引擎为了减少对应用的性能造成的影响,采用了一种比较粗暴的手段,那就是直接限制堆内存的大小。
提示
具体不展开,感兴趣可看文章,介绍更详细。
垃圾回收是怎样进行的
V8 的垃圾回收策略主要是基于分代式垃圾回收机制
,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。
V8 内存结构
V8 的内存结构主要由以下几个部分组成:
新生代(new_space)
:大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。老生代(old_space)
:新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。大对象区(large_object_space)
:存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。代码区(code_space)
:代码对象,会被分配在这里,唯一拥有执行权限的内存区域。map 区(map_space)
:存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单
上图中的带斜纹的区域代表暂未使用的内存,新生代(new_space)被划分为了两个部分,其中一部分叫做 inactive new space,表示暂未激活的内存区域,另一部分为激活状态
新生代
在 V8 引擎的内存结构中,新生代主要用于存放存活时间较短的对象。新生代内存是由两个 semispace(半空间)构成的,内存最大值在 64 位系统和 32 位系统上分别为 32MB 和 16MB,在新生代的垃圾回收过程中主要采用了 Scavenge 算法。
Scavenge 算法是一种典型的牺牲空间换取时间的算法,对于老生代内存来说,可能会存储大量对象,如果在老生代中使用这种算法,势必会造成内存资源的浪费,但是在新生代内存中,大部分对象的生命周期较短,在时间效率上表现可观,所以还是比较适合这种算法。
Scavenge 算法的垃圾回收过程主要就是将存活对象在 From 空间和 To 空间之间进行复制,同时完成两个空间之间的角色互换,因此该算法的缺点也比较明显,浪费了一半的内存用于复制。
对象晋升
当一个对象在经过多次复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种对象从新生代转移到老生代的过程我们称之为晋升。 对象晋升的条件主要有以下两个:
- 对象是否经历过一次 Scavenge 算法
- To 空间的内存占比是否已经超过 25%
默认情况下,我们创建的对象都会分配在 From 空间中,当进行垃圾回收时,在将对象从 From 空间复制到 To 空间之前,会先检查该对象的内存地址来判断是否已经经历过一次 Scavenge 算法,如果地址已经发生变动则会将该对象转移到老生代中,不会再被复制到 To 空间,可以用以下的流程图来表示:
如果对象没有经历过 Scavenge 算法,会被复制到 To 空间,但是如果此时 To 空间的内存占比已经超过 25%,则该对象依旧会被转移到老生代,如下图所示:
之所以有 25%的内存限制是因为 To 空间在经历过一次 Scavenge 算法后会和 From 空间完成角色互换,会变为 From 空间,后续的内存分配都是在 From 空间中进行的,如果内存使用过高甚至溢出,则会影响后续对象的分配,因此超过这个限制之后对象会被直接转移到老生代来进行管理。
老生代
在老生代中,因为管理着大量的存活对象,如果依旧使用 Scavenge 算法的话,很明显会浪费一半的内存,因此已经不再使用 Scavenge 算法,而是采用新的算法 Mark-Sweep(标记清除)和 Mark-Compact(标记整理)来进行管理。 在早前我们可能听说过一种算法叫做引用计数,该算法的原理比较简单,就是看对象是否还有其他引用指向它,如果没有指向该对象的引用,则该对象会被视为垃圾并被垃圾回收器回收。
具体不展开,请看参考链接
如何避免内存泄露
1. 尽可能少地创建全局变量
2. 手动清除定时器
3. 少用闭包
4. 清除DOM引用
5. 弱引用
总结
本文中主要讲解了一下V8引擎的垃圾回收机制,并分别从新生代和老生代讲述了不同分代中的垃圾回收策略以及对应的回收算法,之后列出了几种常见的避免内存泄漏的方式来帮助我们写出更加优雅的代码。如果你已经了解过垃圾回收相关的内容,那么这篇文章可以帮助你简单复习加深印象,如果没有了解过,那么笔者也希望这篇文章能够帮助到你了解一些代码层面之外的底层知识点。
参考
推荐另一篇高质量文章 「硬核 JS」你真的了解垃圾回收机制吗 需要授权转载,尊重原创,这里就没用里面的内容,自行查看吧