V8垃圾回收机制
这部分的阅读资料非常多,整理了下面几个文章,有空常看看。对主要内容做了简单的整理。
引用计数和标记清除
- 引用计数就是判断对象被引用的次数,为 0 时回收,大于 0 就不回收。
缺点: 对象循环引用的时候,就会产生不被回收的情况,从而造成内存泄露。比如:
function fn () {
const obj1 = {}
const obj2 = {}
obj1.a = obj2
obj2.a = obj1
}
fn()
- 标记清除将可达对象标记起来,不可达的对象当垃圾回收。
什么是可达,其实就是树,根节点是全局对象,从根开始向下搜索子节点,在这个树上的能被搜索到就是可达的。
JS 内存管理
垃圾回收不仅仅是上面的两个算法。
JS 中内存的流程:分配内存
–> 使用内存
–> 释放内存
。
- 基础类型:分配固定内存的大小,值存在
栈内存
中 - 引用类型:分配内存大小不固定,指针存在
栈内存中
,指向堆内存
中的对象,通过引用来访问
栈内存存储的基础类型大小固定,所以栈内存都是 操作系统自动分配和释放回收的
堆内存存储的引用类型的大小不固定,所以需要 JS 引擎来手动释放。
chrome 限制了 V8 内存使用大小
64 位约 1.4G/1464MB , 32 位约 0.7G/732MB
之所以做这个限制:
- 是因为浏览器上没有大内存的场景,
- 是因为 V8 的垃圾回收机制的限制 – 清理大量的内存会非常消耗时间,会阻塞单线程的 JS,性能和体验直线下降
V8 回收算法
JS 中对象存活周期有两种,短期和长期的,一个对象经过了多次垃圾回收机制它还存在那就变成长期的了,如果对这种明知收不掉还每次都去做回收的动作就很消耗性能。
– 因此,V8 将堆分为了两个空间:新生代和老生代。新生代是存放存活周期短对象的地方,老生代是存放存活周期长对象的地方
- 新生代:
- 大小 1-8M
- 副垃圾回收器 + Scavenge 算法
- 老生代:
- 大得多
- 主垃圾回收器 + Mark-Sweep && Mark-Compact 算法
Scanvenge 算法
Scavenge 算法是一个典型的牺牲空间换取时间的复制算法
,在占用空间不大的场景上非常适用。
Scavange 算法将新生代堆分为两部分,分别叫 from-space 和 to-space。工作方式也很简单,就是将 from-space 中存活的活动对象复制到 to-space 中,并将这些对象的内存有序的排列起来,然后将 from-space 中的非活动对象的内存进行释放,完成之后,将 from space 和 to space 进行互换,这样可以使得新生代中的这两块区域可以重复利用。
一个对象第一次被分配到 nursery 子代,经过一次回收后还存在新生代就被移动到 intermediate 子代,再下一次还存在就会被 副垃圾回收期 移动到老生代中。
Mark-Sweep(标记清理) 和 Mark-Compact 算法(标记整理)
不和新生代使用一样的算法是因为 老生代 空间大,不适合复制,浪费性能,再说老生代就是大量活着的,来回复制没必要。所以才使用了另外两种标记清理算法。
Orinoco 优化
因为垃圾回收优先于代码执行,老生代的回收有可能插上卡顿现象,为了解决这个问题,V8 进行了优化,项目代号为 orinoco。 提出了 增量标记,惰性清理,并发,并行 的优化方法,详细见下面参考文章吧。