Skip to content

Latest commit

 

History

History
118 lines (75 loc) · 7.01 KB

垃圾回收.md

File metadata and controls

118 lines (75 loc) · 7.01 KB

【JS进阶】垃圾回收、内存优化

我们知道JS通过堆和栈来存储数据(分配内存)

  • 原始数据类型、代码执行时栈帧的位置存储在栈空间
  • 引用类型数据存储在堆空间

但某些数据使用后不再需要,就变成了垃圾数据,如果不回收,内存占用就会越来越多 一般垃圾回收分为手动回收自动回收两种策略

像C/C++是使用手动回收策略,需要代码来决定如何分配和销毁内存,而Javascript/Java/Python等语言都由垃圾回收期来自动释放内存,但并不代表我们不需要关心内存管理(特别是JS,很多开发很容易被自动垃圾回收迷惑,从而忽略内存管理)

一、栈内存回收

先看调用栈中的数据是如何回收的,从执行上下文开始

当一个函数执行上下文被创建并入栈,元素类型数据分配到栈,引用类型数据分配到堆。

与此同时,有一个记录当前执行状态的指针(ESP),执行当前的执行上下文A

当函数A调用完毕出栈时,ESP会下移到下一个执行上下文B,这个下移操作就是销毁上一个执行上下文A的过程,虽然A的执行上下文还保存在栈内存中,但已经是无效内存了,下次再调用其他函数时,会直接覆盖这块内存,用来存放其他函数的执行上下文

所以,当一个函数执行结束之后,JS引擎会通过向下移动ESP来销毁该函数的执行上下文

二、堆内存回收

函数在栈中的执行上下文被销毁后,那还有一些保存在堆中的对象怎么处理呢,那就需要用到垃圾回收器了

2.1 分代收集与主要流程

V8 中,会把堆分为新生代和老生代两个区域

  • 新生代:存放生存时间短的对象 -> 副垃圾回收器 Minor GC
  • 老生代:存放生存时间久的对象 -> 主垃圾回收器 Major GC

垃圾回收器有一套统一执行流程

  • 标记对象
  • 活动对象:还在使用的对象
  • 非活动对象:可以进行垃圾回收
  • 回收非活动对象占据的内存
  • 内存整理:整理内存碎片,留出连续空间,方便后续分配较大连续内存

2.2 副垃圾回收器

新加入的对象都会加入到对象区,对象区快写满时,需要执行一次垃圾回收

对象区域中的垃圾做标记 -> 存活的对象复制到空闲区域中 -> 有序排列 -> 空闲区域没有垃圾碎片 -> 对象区和空闲区的概念发生翻转 -> 无限重复利用

副垃圾回收器一旦监控对象装满了,便执行垃圾回收。同时,副垃圾回收器还会采用对象晋升策略,也就是移动那些经过两次垃圾回收依然还存活的对象到老生代中。

2.3 主垃圾回收器

老生代堆里的对象特点

  • 一个是对象占用空间大
  • 另一个是对象存活时间长

主垃圾回收器采用的算法有两种

  • 标记 - 清除(Mark-Sweep)
  • 标记 - 整理(Mark-Compact)

JS 里还有一种最初级的垃圾回收算法:引用计数,如果没有引用指向该对象,则被回收,但该算法的缺陷是无法处理循环引用的问题

先对垃圾数据进行标记。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

然后清除,这就是标记 - 清除算法

不过对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又引入了另外一种算法——标记 - 整理(Mark-Compact)。

先标记可回收对象,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉这一端之外的内存

2.4 垃圾回收小结

三、垃圾回收的效率优化

JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿(Stop-The-World)。

第一,将一个完整的垃圾回收的任务拆分成多个小的任务,这样就消灭了单个长的垃圾回收任务; 第二,将标记对象、移动对象等任务转移到后台线程进行,这会大大减少主线程暂停的时间,改善页面卡顿的问题,让动画、滚动和用户交互更加流畅。

3.1 优化方案

  • 第一个方案是并行回收,在执行一个完整的垃圾回收过程中,垃圾回收器会使用多个辅助线程来并行执行垃圾回收。
  • 第二个方案是增量式垃圾回收,垃圾回收器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是整个垃圾回收过程中的一小部分工作。
  • 第三个方案是并发回收,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起,难点在于读写锁机制(这里不深入了)

主垃圾回收器就综合采用了所有的方案,副垃圾回收器也采用了部分方案。

四、内存优化

常见三类内存问题

  • 内存泄漏 (Memory leak),它会导致页面的性能越来越差;
    • 在 JavaScript 中,造成内存泄漏 (Memory leak) 的主要原因是不再需要 (没有作用) 的内存数据依然被其他对象引用着。
  • 内存膨胀 (Memory bloat),它会导致页面的性能会一直很差;
  • 频繁垃圾回收,它会导致页面出现延迟或者经常暂停。

4.1 内存泄露常见原因和解决方案

  • 全局变量:尽量避免设置比较大的全局对象
  • 监听器、定时器引用没有及时清除
  • 闭包常驻内存:比较容易形成对象的循环引用,把变量设置null切断联系
  • DOM引用:保留了DOM节点的引用,导致GC没有回收

除了以上,多用ES6的WeakMap/WeakSet,它们都保持对象的弱引用

4.2 内存泄露识别方法

  • 在浏览器中可通过查看memory来观察内存是否有上升的趋势
  • 在Node环境可通过 headdump 抓取内存信息进行分析

参考

Orinoco — 新的 V8 垃圾回收器 by Peter Marshal