说明
图解 Google V8 学习笔记
垃圾数据是怎么产生的?
例子:
window.test = new Object() window.test.a = new Uint16Array(100)
上面代码内存布局图:
在上面的基础上,添加代码执行:
window.test.a = new Object()
此时的内存布局:a 属性之前是指向堆中数组对象的,现在已经指向了另外一个空对象,此时堆中的数组对象就成为了垃圾数据。
V8 虚拟机是怎么实现垃圾回收的?
垃圾回收算法
目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:
通过 GC Root 遍历到的对象,该对象是可访问的(reachable),那么必须保证这些对象应该在内存中保留,称可访问的对象为活动对象;
通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),那么这些不可访问的对象就可能被回收,称不可访问的对象为非活动对象。
什么是 GC Roots?
在浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):
全局的 window 对象(位于每个 iframe 中);
文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
存放栈上变量。
垃圾回收是怎么实现?
一般来说,频繁回收对象后,内存中就会存在大量不连续空间,这些不连续的内存空间称为内存碎片。
垃圾回收的大致流程:
通过 GC Root 标记空间中活动对象和非活动对象。
回收非活动对象所占据的内存:就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
做内存整理:当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。
代际假说 The Generational Hypothesis
代际假说是垃圾回收领域中一个重要的术语,特点:
大部分对象在内存中存活的时间很短:比如函数内部声明的变量,或者块级作用域中的变量,当函数或者代码块执行结束时,作用域中定义的变量就会被销毁。因此这一类对象一经分配内存,很快就变得不可访问;
不死的对象,会活得更久:比如全局的 window、DOM、Web API 等对象。
两个垃圾回收器
V8 的垃圾回收策略,就是建立在代际假说的基础之上的。
在 V8 中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,新生代通常只支持 1~8M 的容量,老生代中存放生存时间久的对象。
主垃圾回收器 -Major GC:主要负责老生代的垃圾回收。
副垃圾回收器 -Minor GC (Scavenger):主要负责新生代的垃圾回收。
副垃圾回收器
新生代中的垃圾数据用 Scavenge 算法来处理。
Scavenge 算法
所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域 (from-space),一半是空闲区域 (to-space),如下图所示:
垃圾回收过程:
- 首先要对对象区域中的垃圾做标记;
- 标记完成之后,就进入垃圾清理阶段。
- 副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来。
- 完成复制后,对象区域与空闲区域进行角色翻转。
角色翻转示意图:
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。副垃圾回收器会采用对象晋升策略,移动那些经过两次垃圾回收依然还存活的对象到老生代中。
主垃圾回收器
主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。
老生代中的对象除了新生代中晋升的对象,还有一些大的对象会直接被分配到老生代里。
两个特点:
对象占用空间大
对象存活时间长
标记 - 清除(Mark-Sweep)
工作过程:
标记过程阶段:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
垃圾的清除过程:主垃圾回收器会直接将标记为垃圾的数据清理掉。
标记清除过程示意图:
标记 - 整理(Mark-Compact)
对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又引入了另外一种算法——标记 - 整理(Mark-Compact)。
工作过程:
先标记可回收对象:这个算法的标记过程仍然与标记 - 清除算法里的是一样的
让所有存活的对象都向一端移动,然后直接清理掉这一端之外的内存。
标记整理过程示意图: