什么是垃圾回收机制?
JS程序在运行过程中,会有很多变量存储在内存中,当代码执行过之后,这些变量我们后边不会再用到了,这个时候JS引擎就会帮我们回收这些已经不会用到的变量,进行内存释放。这个操作是周期性的,但是每次垃圾回收机制必须跟踪哪些变量不会再使用,所以垃圾回收的机制也会不同,一般主要的策略有两种:标记清理和引用计数。
回收策略的解释
标记清理
标记清理是JS最常用的垃圾回收策略,如下:
- 对所有的JS程序而言,都需要创建很多变量,引用类型的引用放在栈中,值则会存储在堆中。
- 垃圾回收策略第一次标记是遍历所有的
可达对象,也就是会用到的或者正在用的变量都会标记,这里我们标记为1,这里的所有可达对象我们称之为"根"。 - 等到遍历完所有可达对象之后,第二次遍历会遍历
所有对象,将不可达的对象标记为0。 - 然后就是等待回收机制对所有标记为0的变量进行一次
垃圾回收,再把所有的可达对象标记去掉,等待下一次的垃圾回收重复以上动作。
标记的方法是有很多种的,这里不一定是我说的这种方法,实际上各浏览器都实现了采用标记清理的垃圾回收机制,不过在运行回收机制的频率上是有所差异的。
引用计数
还有一种不是那么常用的回收策略,思路如下:
- 对每个值都记录它被
引用的次数,声明变量并赋一个引用值时,引用数为1。如果同一个值被赋给了另一个变量,引用数加1。 - 如果保存该值的引用的变量被其他值覆盖了,引用数
减1。 - 当一个值的引用数为
0的时候,就说明无法再访问到这个值了,这样垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。
不过该技术方法在循环引用的情况下,会造成内存永远无法被释放的情况,因此存在一定弊端。
垃圾回收的性能
垃圾回收机制存在的问题主要的原因有几点:
- 垃圾回收会
周期性的运行,如果变量过多,会有性能损失的情况。 - 为了避免性能损失,需要合理的
时间调度。 - 内存过小的设备会因为垃圾回收
拖慢渲染速度。
为了避免以上的情况,垃圾回收的时间调度是很重要的,我们必须保证无论什么时候触发垃圾回收,都要很快的完成回收垃圾的过程。一般来说时间调度的阀值不会是固定的,主要还是根据回收的垃圾占到已分配内存的多少来控制阀值。
内存管理
讲道理我们一般不用关心内存管理的情况,但是出于安全考虑,通常操作系统分配给浏览器的内存比桌面软件要少很多,因为过多的运行JavaScript网页耗尽系统内存的话会导致操作系统的崩溃。所以我们尽可能让我们的程序的内存占用量变小,这样可以提高页面性能,我们有以下办法来做:
- 解除引用:如果数据不在必要,将其置为
null,一般作用于全局变量以及全局对象的属性,局部变量超出作用域会自动解除掉引用。 - 使用const和let关键字来定义变量:因为
const和let作用于块级作用域,这样可以尽早的让垃圾回收机制介入,回收应该回收的内存。 - 隐藏类和删除操作:比如两个类的实例都是由一个类实例化得到的,这个时候V8在后台就会将两个实例共享相同的隐藏类,如果其中一个类在实例化之后修改里面的属性,这个时候两个实例就会对应两个不同的隐藏类,造成性能上的损失,解决办法就是一开始实例化的时候就修改好属性。删除操作同样的,实例化之后删除其中一个实例的属性,也会让两个实例对应两个不同的隐藏类,这个时候也会造成性能损耗,最好的操作就是不用的属性直接置为
null即可。
内存泄露
内存泄露简单理解就是本该销毁的内存无法被销毁,生命周期发生了错位。大部分的内存泄露都是因为不合理的引用造成的,有以下几种情况:
-
局部作用域中定义了全局变量,这个时候变量会注册到
window上,成为window的属性,这样函数执行完变量是无法被销毁的。解决办法就是使用var,let,const关键字来定义变量。 -
闭包不合理使用导致的生命周期错位,导致本该销毁的变量无法被销毁掉。