浏览器内存
- 浏览器内存分为栈和堆
- 变量的声明存在于栈中 (简单类型或者是局部临时变量,这是由系统自动分配的)
- 变量的定义(数组或对象的具体指向)存放于堆中 (ps: 一般存放的就是全局变量,数组对象等引用类型数据,往往由我们的代码去申请)
什么是垃圾
- 不被需要,即为垃圾
- 全局变量随时都可能用到,所以一定不是垃圾
不被需要怎么判定
V8采用的是可达性(reachability)算法来判断堆中的对象该不该被回收
- 具体算法:
- 从根节点(Root)出发去遍历所有的对象
- 可以遍历到的对象,是可达的(reachable)
- 遍历不到的对象,是不可达的(unreachable)
- 在浏览器环境中,根节点有很多种,主要包括以下几种:
- 全局变量Window,位于每个iframe
- 文档 DOM 树
- 存放在栈上的变量
- ...
这些根节点不是垃圾,不可能被回收
怎么进行回收
- 根据判定算法将不可达的数据标记出来,在所有标记完成之后,统一清理内存中不可达的对象
回收后就完了吗?存在什么问题?
- 我们频繁的回收对象后内存中会存在大量不连续的空间,专业名词叫做“内存碎片”
- 当内存中存在大量的内存碎片,这个时候如果需要分配一个大量连续空间,就会出现内存不足的情况
- 所以在我们清理完后需要进行内存的整理(但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。)
垃圾会回收的时机?什么时候进行垃圾回收合适?
- 浏览器在进行垃圾回收时会暂停JS 脚本的执行,等回收完后才能继续JS执行
- 对于普通应用这样没什么问题,但对于JS游戏,动画对连贯性要求比较高的应用,如果暂停就会造成页面卡顿
- 接下来我们讨论下在什么时候进行回收才能避免长时间的暂停
分代收集
浏览器将数据分为两种,一种是「长久」对象,一种是「临时」对象。
- 临时对象:
- 大部分对象在内存中存活时间很短
- 比如函数内部声明的变量,或者块级作用域中存在的变量。在函数或者块级代码执行结束后,作用域中的变量就会被销毁
- 像这种很快就不会被用到的对象就该被尽快回收
- 长久对象:
- 生命周期很长的对象,比如全局的window,DOM,Web API 等
- 这类对象很容易随时被用到,所以应该被慢点回收
这两种对象对应不同的回收策略,所以V8 把堆分为新生代和老生代两个区域,新生代对象存放临时对象,老生代对象存放长久对象。
并且让副垃圾回收器,主垃圾回收器分别负责新生代、老生代垃圾的回收。
接下来我们讨论下V8以怎样的策略来实现高效的垃圾回收
副垃圾回收器(负责新生代垃圾回收)
新生代中的对象主要通过Scavenge算法进行回收,Scavenge的具体实现中主要采用的是Cheney算法,如下图了解整个过程:
- 新生代被分为两个区域:一半对象区(From空间),一半闲置区(To空间)
- 对象区是处于使用中的活跃地带,而闲置区是非使用状态的空闲地带
- 新加入对象分配空间时,首先从对象区分配,当开始垃圾回收时,将From空间的对象复制到To空间,接下来清除整个From空间的对象,释放对象区
- 完成复制后,From 空间 和 To 空间进行角色互换
- 简而言之,Cheney 算法的核心思想是采用**对象,一种是复制的思想去实现的简图如下:
总结: 新生代垃圾回收的缺点显而易见,它只能使用到堆内存的一半。优点在于它只复制存活的对象,对于生命周期短的场景存活对象只占少部分,所以在时间效率上有着不错的表现。对于副垃圾回收器的复制思想是不存在内存整体这个步骤的,也回答了我们上述所说的:不是所有垃圾回收都需要内存整理。
上述所说的是纯Scaenge算法,但在分代回收的算法中,Form 空间的对象进入到To空间时是需要检查的,**在一定条件下,需要将存活时间较长的对象移入到老生代空间中,这个过程被称为晋升。
对象的晋升有两个条件: * 对象是否经历过Scaenge回收 * 当To空间使用度超过25%时,直接将对象直接晋升到老生代空间中
如下图描述两个条件
这个也很好理解,经历过Scaenge的对象肯定是存活时间较久的了
讲完新生代算法,接下来我们看下老生代垃圾回收的具体策略
主垃圾回收器(负责老生代垃圾回收)
特点:
对象占用空间大
对象存活时间久
基于上述的对象特点,老生代主要采用**标记-清除和**标记-整理的算法来进行垃圾回收
标记-清除(Mark-Sweep)算法
- Mark-Sweep在标记阶段遍历堆内存中的所有对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象
- 也就是说,Scavenge只复制活着的对象,而Mark-Sweep只清除死了的对象。活对象在新生代中只占较少部分,死对象在老生代中只占较少部分,这就是两种回收方式都能高效处理的原因
- 但是这个算法有个比较大的问题是,内存碎片太多。如果出现需要分配一个大内存的情况,由于剩余的碎片空间不足以完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的
- 所以在此基础上提出Mark-Compact算法
标记-整理(Mark-Compact)算法
- Mark-Compact在标记完存活对象以后,会将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的所有内存
流程可以参照下图
直接将标记为垃圾的数据清理掉。
多次标记-清除后,会产生大量不连续的内存碎片,需要进行内存整理。
分代收集的总结
将堆分为新生代与老生代,多回收新生代,少回收老生代。
这样就减少了每次需遍历的对象,从而减少每次垃圾回收的耗时。