v8内存模型
理解内存管理方式对程序员更好的开发应用和调优至关重要,在此我们先来引用一张来自博客Visualizing memory management in V8 Engine的v8内存模型图:
v8内存模型 图-1
在进一步了解垃圾回收机制前我们需要对内存分配的布局有整体认识,如图所示传统内存模型分为堆区和栈区,众所周知栈区承载函数调用(入栈),返回(出栈)同时OS会把函数中局部变量静态类型数据如数字,字符串非配到栈区。函数中的引用类型则通过指针指向堆区地址。
因此堆是存放对象,函数,包装对象等引用类型的分配区。由于栈随着函数执行结束,出栈后所分配的局部静态类型变量数据也会随之释放,因此不用我们过多关心,而由引用类型指针指向的应用类型存放在堆中并不会随之释放,因此我们探讨的所谓内存回收主要以如何回收堆内存为主。
如图所示,堆区(图中Heap)又分为几个逻辑区,我们主要关注的有:
- Young generation,目前短暂存活的对象
- Old generation,长期存活的对象
- Large object space,大对象区,存放大对象,此区对象体积大应避免复制
- Code space, JIT编译执行区
对v8内存模型结构有整体认识后我们将进一步探索v8中两种不同的内存回收机制Minor GC和Major GC,这两种内存回收机制分别用于对young区和堆中非”新世代区“的垃圾回收
Major GC
Major GC主要为标记清除和碎片整理两个阶段。
标记清除
简单来说GC从root序列开始对对象的所组成的图进行深度优先遍历,对能够抵达的节点标记为存活,如下图所示(图片来自维基百科)。
深度优先遍历标记 图-2
由于简单的深度遍历标记算法在确定对象为“可达”或“不可达”两种状态前需阻塞程序的运行(程序运行中可能会改变状态),为进一步改善算法因此演变除了Tri-color marking算法,该算法将需要遍历的图中各节点状态分为三类.
- white-候选节点序列,最后留在此序列中的对象会被回收
- black-已经经过扫描确认,不会被收集的对象。
- grey-该序列中的节点的特征为1.序列中的节点都是"可达"的存活节点。2以该序列中节点为根的子路径中的节点状态未知,可能正在遍历检测或者还未检测到。因此grey中的节点也为不可回收节点。
(图片来自维基百科)
三色标记算法 图-3
简单来说“三色标记”标记算法使用DFS遍历从White候选序列中选取存活节点到Black序列中,Black序列为存活节点。而grey序列中个节点则为其子路径节点是否可回收状态未知,但序列中节点为不可回收节点,grey中的节点最终会被放入black序列中。最后剩在White中的节点为"未存活"的节点并进行垃圾回收。
关于回收,GC会把可回收对象排成空闲列表,当有新的对象需要分配空间时,只需要根据相关内存分配算法从列表中选取合适内存块进行分配。
碎片整理
伴随着内存回收,内存中会产生碎片(不连续的空间,分为内部或外部碎片),碎片的产生不利于内存的分配,因此碎片整理阶段则是使用碎片整理技术减少内存碎片(外部碎片),大致思路是把存活的对象移动到连续的内存地址上去,尽可能保证已分配的内存块(页)之间没有空隙。
到此我们知道了Major GC的整体思路,通过深度优先遍历标记可达的存活节点->将可回收节点排为空闲列表以待分配->对内存空间进行碎片整理。
Minor GC(Scavenger)
上文中我们介绍了Major GC算法它作用于非”新世代区“包括”旧世代区“和”大对象区“等,可能有朋友会问为什么要把堆中分为old和young区呢? 根据所谓的”世代假说(The Generational Hypothesis)“新分配的内存空间很大可能也需要被回收,需要回收的内存更多,而"旧世代"存放的为生存两个以上回收周期的对象,相对来说需要回收内存可能更少,因此Minor GC更适合对”新世代区”小尺寸以及较少生存对象进行回收。同时根据不同的内存回收的数量等特点将堆的内存回收区分为”新世代“和”旧世代“。
Minor GC 如何工作
从图-1中我们看到,“新世代”又被分为两个区分别为From空间和To空间,From空间是实际内存分配的空间,而To空间是垃圾回收后存放生存对象的空间,这也意味着“新世代区”只有一半的空间用于时间的存储。
当需要分配from空间且发现空间不足时,Minor GC会通过对被称为old-to-new(一个存放在old区中,指向young区对象的指针集合)的集合进行遍历,遍历到的节点代表存活节点会有两种种情形:
- 首次被标记为存活的对象,第一次被GC发现的存活对象会被复制到to空间(复制按连续地址分配)。根据”时代假说“多数新分配的空间都需要被回收,意味着根据这个假说”新世代“中只有少数存活的对象需要被复制到to空间中,因此减少了数据的拷贝工作,最差情况则是所有对象都拷贝的to空间中。
- 本次GC是第二次被标记为存活,第二次存活的对象会被复制到old区。
随着存活对象的移动还需要对指向这些已经被移动对象的指针进行更新,被移动对象的原始对象会有一个转发地址指向新地址用于其他对象对其引用的查找。
最后留在from空间中未被移动对象就是需要被回收的对象,此时只需要把from空间和to空间逻辑上翻转,from空间作为to空间,to空间作为from空间使用便等同于完成了回收工作。注意到在Minor GC中并不需要进行单独的碎片整理,因为当从from空间向to空间进行数据复制时是按to中连续地址进行复制。
关于性能
无论是Major GC还是Minor GC都是同步方式进行,意味着两种GC的执行会阻塞主线程并暂停当前正在执行的任务,众所周知主线程无论是在浏览器还是nodejs中都是稀缺资源以及承担者繁重任务,浏览器中主线程需要不但需要负责javascript执行还需要负责layout和部分paint任务,因此GC过长会影响用户体验。v8中使用多线程并行技术来帮助GC。
Minor GC(Scavenger)
v8通过多线程方式完成内存回收,多个线程同时执行对象的访问和移动,因此各线程之间需要通过读写同步避免访问冲突,每个线程会对转移的对象留下”转发指针“以便其他指针的更新。
多个协助线程与主线程并行GC 图-4
Major GC
对于Major GC,v8会在主线程执行时启用多线程同时执行GC标记工作,当触发GC时,主线程会暂停任务执行,与协助线程共同完成碎片整理,空闲列表生成以及指针更新的工作。
Major GC-多线程 图-5
(图-4与图-5来自Trash talk: the Orinoco garbage collector)
总结
对垃圾回收机制的整体了解可以帮助应用程序开发程序员更深入理解编程语言的行为,例如在js中weakmap这个数据结构,我们都知道其每个键都是”弱引用“有利于垃圾回收,之所以弱引用利于回收在于”弱引用“在GC标记环节键引用的对象会被认为不可达,因此在一次GC后对应对象会被回收。同时对内存模型及GC机制理解更有利于我们排查分析程序内存泄露等问题。