1. 前言
- js垃圾回收机制
- MDN 垃圾回收机制
- 这是原文,支持原创,可以直接点进进去 忽略掉本文的
- 自己还在搬运一遍的目的就是为了熟悉,也为了以后自己查看,保存链接的,说不定TA人的链接啥时候不能用了
2. 是什么 what
不管什么程序语言,内存生命周期基本是一致的:
1.分配你所需要的内存
2.使用分配到的内存(读、写)
3.不需要时将其释放\归还
在 JavaScript 中,当创建变量时,系统会自动给对象分配对应的内存,来看下面的例子:
var n = 123; // 给数值变量分配内存 var s = "azerty"; // 给字符串分配内存 var o = { a: 1, b: null }; // 给对象及其包含的值分配内存 // 给数组及其包含的值分配内存(就像对象一样) var a = [1, null, "abra"]; function f(a){ return a + 2; } // 给函数(可调用的对象)分配内存 // 函数表达式也能分配一个对象 someElement.addEventListener('click', function(){ someElement.style.backgroundColor = 'blue'; }, false);
3. JavaScript 数据分类
- 基本类型:这些类型在内存中会占据固定的内存空间,它们的值都保存在栈空间中,直接可以通过值来访问这些;
- 引用类型:由于引用类型值大小不固定,
栈内存
中存放地址指向堆内存
中的对象,是通过引用
来访问的。
栈内存和堆内存
- 栈内存中的基本类型,可以通过操作系统直接处理;
- 而堆内存中的引用类型,正是由于可以经常变化,大小不固定,因此需要 JavaScript 的引擎通过垃圾回收机制来处理。
垃圾回收
- 所谓的垃圾回收是指:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间。
- Javascript 具有
自动垃圾回收
机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用
的变量,然后释放
掉其占用的内存。
局部变量和全局变量
- JavaScript中存在两种变量:局部变量和全局变量。
全局
变量的生命周期会持续到页面卸载
;- 而
局部
变量声明在函数
中,它的生命周期从函数执行开始,直到函数执行结束
,- 在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量
不再被使用
,它们所占有的空间就会被释放
。- 不过,当局部变量被外部函数使用时,其中一种情况就是
闭包
,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收
。
4. V8 垃圾回收
4.1 通过 GC Root 标记空间中活动对象和⾮活动对象
- ⽬前 V8 采⽤的
可访问性
算法来判断堆中的对象是否是活动对象。这个算法是将⼀些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中所有对象- 通过 GC Root 遍历到的对象是可访问的,必须保证这些对象应该在内存中保留,可访问的对象称为
活动对象
- 通过 GC Roots 没有遍历到的对象是
不可访问
的,这些不可访问的对象就可能被回收,不可访问的对象称为⾮活动对象
。- 这也是自
2012
年起 所有浏览器都使用的标记清除算法
4.2 回收⾮活动对象所占据的内存
- 其实就是在所有的标记完成之后,统⼀清理内存中所有被标记为可回收的对象
4.3 内存整理
- ⼀般来说,
频繁回收
对象后,内存中就会存在⼤量不连续
空间,这些不连续的内存空间称为内存碎⽚
。当内存中出现了⼤量的内存碎⽚之后,如果需要分配较⼤的连续内存时,就有可能出现内存不⾜
的情况,所以最后⼀步需要整理这些内存碎⽚。这步其实是可选的,因为有的垃圾回收器不会产⽣内存碎⽚。- 以上就是⼤致的垃圾回收流程。⽬前 V8 使用了两个垃圾回收器:
主垃圾
回收器和副垃圾
回收器。下面就来看看 V8 是如何实现垃圾回收的。- 在 V8 中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象:
- 图示
- 新⽣代通常只⽀持
1~8M
的容量,⽽⽼⽣代⽀持的容量就⼤很多。对于这两块区域,V8分别使⽤两个不同的垃圾回收器,以便更⾼效地实施垃圾回收:副垃圾回收器:负责新⽣代的垃圾回收。
主垃圾回收器:负责⽼⽣代的垃圾回收。
5. 副垃圾回收器--新生代
- 副垃圾回收器主要负责
新⽣代
的垃圾回收。大多数的对象最开始都会被分配在新生代,该存储空间
相对较小
,分为两个空间:from 空间(对象区)和 to 空间(空闲区)- 新加⼊的对象都会存放到对象区域,当对象区域快被写满时,就需要执⾏⼀次垃圾清理操作:首先要对对象区域中的垃圾做标记,标记完成之后,就进入垃圾清理阶段。副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来。这个复制过程就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了:
- 图示
- 完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域,这种算法称之为 Scavenge 算法,这样就完成了垃圾对象的回收操作。同时,这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去:
- 图示
- 不过,副垃圾回收器每次执⾏清理操作时,都需要将存活的对象从对象区域复制到空闲区域,复制操作需要时间成本,如果新⽣区空间设置得太⼤了,那么每次清理的时间就会过久,所以为了执⾏效率,⼀般新⽣区的空间会被设置得⽐较⼩。 也正是因为新⽣区的空间不⼤,所以很容易被存活的对象装满整个区域,副垃圾回收器⼀旦监控对象装满了,便执⾏垃圾回收。同时,副垃圾回收器还会采⽤对象晋升策略,也就是移动那些经过两次垃圾回收依然还存活的对象到⽼⽣代中
6. 主垃圾 回收器 ---老生代
- 主垃圾回收器主要负责⽼⽣代中的垃圾回收。除了新⽣代中晋升的对象,⼀些⼤的对象会直接被分配到⽼⽣代⾥。因此,⽼⽣代中的对象有两个特点:
- 对象占⽤空间⼤;
- 对象存活时间⻓。
由于⽼⽣代的对象⽐较⼤,若要在⽼⽣代中使⽤
Scavenge
算法进⾏垃圾回收,复制这些⼤的对象将会花费较多时间,从⽽导致回收执⾏效率不⾼,同时还会浪费空间。所以,主垃圾回收器采⽤标记清除的算法
进⾏垃圾回收。这种方式分为标记和清除两个阶段:
- 标记阶段: 从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
- 清除阶段: 主垃圾回收器会直接将标记为垃圾的数据清理掉。
- 2个阶段如图
- 对垃圾数据进⾏标记,然后清除,这就是标记清除算法,不过对⼀块内存多次执⾏标记清除算法后,会产⽣⼤量不连续的内存碎⽚。⽽碎⽚过多会导致⼤对象⽆法分配到⾜够的连续内存,于是⼜引⼊了另外⼀种算法——标记整理。
- 这个算法的标记过程仍然与标记清除算法⾥的是⼀样的,先标记可回收对象,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉这⼀端之外的内存:
- 图示
7. 全停顿
- 我们知道,JavaScript 是
单行线语言
,运行在主线程上。一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿- 主垃圾回收器执行一次完整的垃圾回收流程如下图所示:
- 在 V8 新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大。但老生代中,如果在执行垃圾回收的过程中,占用主线程时间过久,主线程是不能做其他事情的,需要等待执行完垃圾回收操作才能做其他事情,这将就可能会造成页面的卡顿现象。
- 为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,这个算法称为增量标记算法。如下图所示
- 使用增量标记算法可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行代码时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。
8. 减少垃圾回收
- 虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价较大,所以应该尽量减少垃圾回收:
- 对数组进行优化: 在
清空
一个数组
时,最简单的方法就是给其赋值为[ ]
,但是与此同时会创建一个新的空对象,可以将数组的长度
设置为0
,以此来达到清空数组的目的。- 对object进行优化: 对象尽量
复用
,对于不再
使用的对象,就将其设置为null
,尽快被回收。- 对函数进行优化: 在循环中的函数表达式,如果可以
复用
,尽量放在函数的外面
。