JS的垃圾回收机制

283 阅读5分钟

什么是垃圾回收机制?

JS程序在运行过程中,会有很多变量存储在内存中,当代码执行过之后,这些变量我们后边不会再用到了,这个时候JS引擎就会帮我们回收这些已经不会用到的变量,进行内存释放。这个操作是周期性的,但是每次垃圾回收机制必须跟踪哪些变量不会再使用,所以垃圾回收的机制也会不同,一般主要的策略有两种:标记清理引用计数

回收策略的解释

标记清理

标记清理是JS最常用的垃圾回收策略,如下:

  • 对所有的JS程序而言,都需要创建很多变量,引用类型的引用放在栈中,值则会存储在堆中。
  • 垃圾回收策略第一次标记是遍历所有的可达对象,也就是会用到的或者正在用的变量都会标记,这里我们标记为1,这里的所有可达对象我们称之为"根"
  • 等到遍历完所有可达对象之后,第二次遍历会遍历所有对象,将不可达的对象标记为0。
  • 然后就是等待回收机制对所有标记为0的变量进行一次垃圾回收,再把所有的可达对象标记去掉,等待下一次的垃圾回收重复以上动作。

标记的方法是有很多种的,这里不一定是我说的这种方法,实际上各浏览器都实现了采用标记清理的垃圾回收机制,不过在运行回收机制的频率上是有所差异的。

引用计数

还有一种不是那么常用的回收策略,思路如下:

  • 对每个值都记录它被引用的次数,声明变量并赋一个引用值时,引用数为1。如果同一个值被赋给了另一个变量,引用数加1
  • 如果保存该值的引用的变量被其他值覆盖了,引用数减1
  • 当一个值的引用数为0的时候,就说明无法再访问到这个值了,这样垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。

不过该技术方法在循环引用的情况下,会造成内存永远无法被释放的情况,因此存在一定弊端。

垃圾回收的性能

垃圾回收机制存在的问题主要的原因有几点:

  • 垃圾回收会周期性的运行,如果变量过多,会有性能损失的情况。
  • 为了避免性能损失,需要合理的时间调度
  • 内存过小的设备会因为垃圾回收拖慢渲染速度

为了避免以上的情况,垃圾回收的时间调度是很重要的,我们必须保证无论什么时候触发垃圾回收,都要很快的完成回收垃圾的过程。一般来说时间调度的阀值不会是固定的,主要还是根据回收的垃圾占到已分配内存的多少来控制阀值。

内存管理

讲道理我们一般不用关心内存管理的情况,但是出于安全考虑,通常操作系统分配给浏览器的内存比桌面软件要很多,因为过多的运行JavaScript网页耗尽系统内存的话会导致操作系统的崩溃。所以我们尽可能让我们的程序的内存占用量变小,这样可以提高页面性能,我们有以下办法来做:

  • 解除引用:如果数据不在必要,将其置为null,一般作用于全局变量以及全局对象的属性,局部变量超出作用域会自动解除掉引用。
  • 使用const和let关键字来定义变量:因为constlet作用于块级作用域,这样可以尽早的让垃圾回收机制介入,回收应该回收的内存。
  • 隐藏类和删除操作:比如两个类的实例都是由一个类实例化得到的,这个时候V8在后台就会将两个实例共享相同的隐藏类,如果其中一个类在实例化之后修改里面的属性,这个时候两个实例就会对应两个不同的隐藏类,造成性能上的损失,解决办法就是一开始实例化的时候就修改好属性。删除操作同样的,实例化之后删除其中一个实例的属性,也会让两个实例对应两个不同的隐藏类,这个时候也会造成性能损耗,最好的操作就是不用的属性直接置为null即可。

内存泄露

内存泄露简单理解就是本该销毁的内存无法被销毁,生命周期发生了错位。大部分的内存泄露都是因为不合理的引用造成的,有以下几种情况:

  • 局部作用域中定义了全局变量,这个时候变量会注册到window上,成为window的属性,这样函数执行完变量是无法被销毁的。解决办法就是使用varletconst关键字来定义变量。

  • 闭包不合理使用导致的生命周期错位,导致本该销毁的变量无法被销毁掉。