垃圾回收机制
谈谈 JS 垃圾回收机制?
V8 垃圾回收机制
常见的内存泄漏原因:
全局变量过多引起内存泄漏
闭包
dom 事件未清除
循环引用
eval
垃圾收集器会定期(周期性)的找出那些不再继续使用的变量,释放其内存,因为开销比较大,所以会按照固定的时间间隔周期性的执行
最常用的两种方法:标记清除(常用)和引用计数
标记清除:变量进入作用域,进行标记,离开作用域进行清除回收
引用计数:就是跟踪记录每个值被引用的次数,引用一次加 1,删除减 1,引用计数为 0 时,进行回收,(循环引用可能会导致内存泄漏)
V8 会把堆分为新生代和老生代
新生代(副垃圾回收器)
- 存放生存时间短的对象
- 通常只支持 1 ~ 8M 的容量
- 分为对象区和空闲区
- 对对象区域中的垃圾做标记,标记完成进入垃圾清理阶段,把存活的对象复制到空闲区域,把这些对象有序的排列起来
- 完成复制后,对象区和空闲区进行角色翻转
老生代(主垃圾回收器)
- 对象存放生存时间久
- 对象占用空间大
- 采用:标记清除算法 和 标记压缩算法
副垃圾回收器采用对象晋升策略:移动那些经过两次垃圾回收依然还存活的对象到老生代中
V8 实现了精准式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
新生代算法
新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。
在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。
新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动。
算法会检查 From 空间中存活的对象并复制到 To 空间中,如果失活的对象就会销毁。
当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代算法
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是 标记清除算法 和 标记压缩算法。
在将算法前,先来说下什么情况下对象会出现在老生代空间中:
● 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间。
● To 空间的对象占比大于 25%。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
以下情况会启动 标记清除算法:
● 某一空间没有分块的时候
● 空间中对象超过一定限制
● 空间不能保证新生代中的对象移动时
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动 压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清除不需要的内存。
垃圾回收
Javascript中的内存管理是自动执行的,而且是不可见的。在我们创建基本类型、对象、函数
时,这些都需要内存,当不需要某样东西时,JavaScript引擎会利用垃圾回收机制清除它。
在局部作用域中,当函数执行完毕,局部变量也就没必要存在了,因此垃圾收集器很容易
做出判断回收。但是全局变量什么时候需要自动释放内存空间很难判断,因此在开发中,
需要尽量避免使用全局变量。
可达性
JavaScript 中内存管理的主要概念是可达性。
当一些值以某种方式可访问或可用,它们会被存储在内存中,叫做可达值。
- 一些固有可达值,由于显而易见的原因无法删除。例如:
- 本地函数的局部变量或参数
- 当前嵌套调用链上的其他函数的变量和参数
- 全局变量等
这些值 称为 根
- 如果引用或引用链 可以从根访问任何其他值,则认为该值是可访问的。
- 一个对象引用另一个对象的属性,则该对象是为可达性。
JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除哪些不可访问的对象。
内部算法
v8 的垃圾回收策略基于分代式垃圾回收机制。将内存分为新生代和老生代,分别采用不同
的算法。
新生代采用 Scavenge 算法(赋值算法)
Scavenge 为新生代采用的算法,是一种采用复制的方式实现的垃圾回收算法。它将内存分
为 from 和 to 两个空间。每次 gc,会将 from 空间的存活对象复制到 to 空间。然后两个空间角色
对换(又称反转)。该算法是牺牲空间换时间,所以适合新生代,因为它的对象生存周期较短。
老生代采用 Mark-Sweep(标记清除)和 Mark-Compact(标记整理)
- 标记-清除算法,定期执行一下“垃圾回收”步骤:
- 垃圾回收器获取根并“标记”它们
- 然后访问并标记所有来自它们的引用
- 然后访问标记的对象 并 标记它们的引用
- 以此类推,知道有为访问的引用为止
- 除了标记的对象外,所有对象都被删除。
Mark-Compact 算法(标记整理)
- 标记清除存在一个问题,清除死亡对象后会造成内存空间不连续,这时候 v8 会使用
Mark-Compact 算法(标记整理),它会在标记完成之后将活着的对象往一端移动,移动完成
后直接清理掉边界外的内存。
Reference Counting(引用计数算法)
- 引用计数,就是记录每个对象被引用的次数,每次新建对象、赋值引用和删除引用的同时
更新计数器,如果计数器值为 0 则直接回收内存。 很明显,引用计数最大的优势是暂停时间短
优化
增量回收:如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费
一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分,然后
各个部分分别执行,这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。空闲时间收集:垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。
v8 的内存限制
- 64 位系统最大约为 1.4G
- 32 位系统最大约为 0.7G