JS|JS 垃圾回收
在 JS 中 值类型数据存储在 栈空间中,引用类型的数据存储在堆空间中。有些数据被使用之后,就不需要了,我们需要将这些 垃圾数据 进行回收从而释放内存空间,防止这些垃圾数据堆积在 内存中。
JS 如何 回收垃圾
在 JS 中,垃圾数据 由 垃圾回收器 来释放,并不需要手动通过代码释放。数据主要存储在栈 和 堆 中,所以我们分两种情况。
1 调用栈中的数据回收
文章图片
代码示例 调用栈详情
文章图片
调用栈详情 当执行到 bar() 这段代码的时候,此时的调用栈 情形如图,此时有个一记录当前执行状态的指针(称为 ESP),指向 bar 函数执行上下文, 记录当前 正在执行到的 地方。当 bar 函数执行完, JS 引擎会 销毁 bar 函数执行上下文,指针移动到上一个 执行上下文,也就是 foo 函数的执行上下文,这个 向下移动的操作就是 销毁 bar 函数执行上下文的过程。
文章图片
1
文章图片
2 总结:一个函数执行结束之后,JavaScript引擎会通过向下移动ESP来销毁该函数保存在栈中的执行上下文。
2 堆中数据的回收
当 foo 函数执行完成后, ESP 就指向了 全局执行上下文,bar 函数执行上下文 和 foo 函数执行上下文 都被销毁,但是 堆 当中的 两块内存依然 存储着数据。要回收堆当中的数据,就要用到 JS 中的垃圾回收器了。
在垃圾回收中有一个重要的概念:代际假说,所有的垃圾回收策略都是建立在该假说之上的。
代际假说的两个特点: 1. 大部分对象在内存中存在的时间很短,简单来说,就是很多对象?经分配内存,很快就变得不可访问; 2. 不死的对象,会活得更久。
垃圾回收算法很多,但不是 一种就能处理所有的情况,需要根据不同的情况,采取不同的算法,所以 JS 把 堆分为 新生代 和 老生代 两个区域, 新生代存放的是生存时间短的对象,老生代存放的生存时间久的对象。
新生代 通常只支持 1-8M 的容量,老生代的容量就大很多。 JS 使用两种不同的垃圾回收器来处理这两块区域的垃圾回收。
1. 副垃圾回收器,主要处理新生代的垃圾回收;
2. 主垃圾回收器,主要负责老生代的垃圾回收。
垃圾回收的工作流程
1. 标记空间中活动对象(还在使用的对象)和 非活动对象(可以进行垃圾回收的对象);
2. 回收非活动对象所占的内存,就是在所有标记完成后,统一清除内存中所有被标记为可回收的对象;
3. 内存整理。通常情况下,在进行垃圾回收后,内存中就会出现 大量不连续的空间,称为 内存碎片。当内存中出现大量 内存碎片后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以我们需要整理这些 内存碎片(有些垃圾回收器不会产生内存碎片,所以这步骤是可选操作)。
副垃圾回收器
主要负责新生区的垃圾回收。通常情况下,大多数小的对象会被分配到 新生区,所以这个区域大小不大,但是操作频繁。
新生区中 采用 Scavenge 算法,就是把 新生区对半划分为两个区域,一半是对象区域,一半是空闲区域
文章图片
堆空间 新加入的对象都会存放到对象区域,当对象区域要被写满的时候,就需要执行一次垃圾清理。
在垃圾回收过程中,首先对 对象区域 内的垃圾做标记,标记完成后,进入垃圾清理阶段, 副垃圾回收器 会把 还在使用的对象 复制到空闲区域,并且有序的排列起来,相当于完成了碎片的整理,复制完成后 空闲区域就没有了内存碎片。
完成复制后,对象区域 和 空闲区域 角色进行交换,原来的对象区域变成空闲区域,原来的空闲区域变成对象区域,这样就完成了垃圾回收操作,同时这种 角色的对调 能让新生区的这两块区域无限重复使用下去。
由于 新生区采用了 Scavenge 算法,每次执行清理操作,都要进行一次复制,如果新生区空间设置太大,那么 复制的时间成本会变大,清理时间就会更久,所以 为了执行效率,一般新生区的空间会被设置的比较小。但也就是因为空间小,所以 还在使用的对象很容易存满整个区域,为了解决这个问题, JS 采用了 对象晋升策略,就是 经过两次垃圾回收依然存活的对象,会被移到老生区。
主垃圾回收器
主要负责老生区的垃圾回收。除了新生区晋升的对象,一些大的对象会被分配到老生区,因此,老生区的对象有两个特点:占用空间大 和 存活时间长。
由于老生区的对象比较大,采用 Scavenge 算法在复制阶段 会导致 消费大量时间,效率不高,同时还会浪费一半的空间。所以,主垃圾回收器 采用 标记 - 清除(Mark-sweep)的算法进行垃圾回收。
1. 标记过程阶段,从一组根元素开始,遍历这组根元素,在这个遍历过程中,能到达的元素称为 活动对象,没有到达的元素就可以判断为 垃圾数据 。
文章图片
3 当 bar 函数执行结束,ESP 向下移动执行 foo 函数执行上下文,这时候遍历 调用栈,不会找到 引用 1002地址的遍历,编辑为垃圾数据,1001地址有变量在引用,标记为活动对象。
2. 垃圾清除过程,当采用 标记-清除法后,会产生 大量不连续的内存碎片,会导致无法分配到足够的连续内存,于是就产生另一种算法:标记-整理(Mark - Compact),这个过程是将 所有 还在使用的对象 都移动到一端,然后直接清除掉边界之外的内存。
文章图片
标记清楚和标记整理 全停顿
V8 使用 副垃圾回收器 和 主垃圾回收器处理垃圾回收, 不过 JS 是单线程,一旦执行 垃圾回收算法,其他 JS 脚本都会暂停,等待垃圾回收结束才恢复执行,我们称这种行为 为 全停顿(Stop-The-World)。
在 新生区 中的垃圾回收,因其空间小,且 活动对象少,所以全停顿的影响不但,但是 老生区不一样。如果垃圾回收 占用时间越久,那么这个过程中,主线程会被完全占用,不能处理其它事情,比如 页面正在执行 动画,因为垃圾回收器,导致 动画在一段时间无法执行,会使页面出现卡顿现象。
文章图片
全停顿 为了降低 老生区的垃圾回收造成的卡顿,V8 将标记过程分为一个个子标记过程,同时让垃圾回收标记 和 JS 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为 增量标记(Incremental Marking)算法
文章图片
增量标记 采用 增量标记,能将一个完整的垃圾回收任务拆分成很多小任务,这些小任务执行时间较短,可以穿插在其他 JS 任务中执行,比如 动画效果,就不会使页面出现卡顿。
总结
JS 中的 简单数据类型 和 引用数据类型 分别存放在 栈 和 堆 中;
栈中的数据 通过 ESP 向下移动 销毁保存在 栈中的数据;
堆分为两块区域:新生区 和 老生区,主要通过 副垃圾回收器 和 主垃圾回收器 处理垃圾;
副垃圾回收器 采用 Scavenge 算法 将 新生区分为 对象区域 和 空闲区域,通过两个区域不断替换角色来无限使用;
主垃圾回收器 采用标记清除法,标记整理法,增量标记法 进行垃圾回收;
【JS|JS 垃圾回收】无论 主 副垃圾回收器,流程都是 标记 ----清除 ----- 整理 这几个步骤。