一篇带给你 V8 GC 的实现

网站建设3年前发布
41 0 0

20230305203205e162b05024a86d8609a834b944cda2fe93647d156,前言:GC 是一个古老、复杂并且很 Cool 的技术,本文大概介绍一下早期 V8 中关于 GC 实现的部分,代码版本 0.1.5,早期版本利于快速理解整体的逻辑,因为现代版本已经非常复杂。,首先看一下 Handle 一般的用法,Handle 是 GC 非常核心的概念。,这两句代码里涉及了两个核心的概念。首先看看 HandleScope。,首先看一下 Data 数据结构,Data 有三个字段 :,接着继续看 current 类静态变量,这个变量记录了当前用于分配 Handle 的地址信息,接着看 HandleScope 类本身。HandleScope 是基于 RAII 机制管理一个函数中,多个 Handle 创建和销毁的对象。HandleScope 在构造函数中记录了当前的上下文,然后在析构函数中进行恢复。我们看到在 HandleScope 的构造函数中把当前上下文记录在 previous_ 里,然后重置 extensions,表示该 HandleScope 还没有申请内存,因为新的 HandleScope 会沿用前一个 HandleScope 的空闲内存,因为 V8 只把 current 赋值给 previous_ ,然后接着使用 current 去分配内存,只有当 HandleScope 内存不够时,才会分配新的内存块。看一下下面的图。,20230305203258743acaf38167f61b836016cf468e94a9c297c4708,首先看一下上图的左边,当我们创建第一个 HandleScope 时,该 HandleScope 对象就会保存当前 HandleScope 的上下文,这时候当前 HandleScope 上下文是 NULL,然后创建第一个 Handle 时因为 next == limit == NULL,所以需要申请一块内存,即 extensions 为 1。当创建第二个 Handle 后,就变成了左上角的样子。接着我们又定义了第二个 HandleScope,那么首先 第二个 HandleScope 同样在 previous_ 中记录当前的上下文,即 current 的值,接着在第二个 HandleScope 中分配一个 Handle 就变成了右图的样子。接下来看退出 HandleScope 时的逻辑,从代码中可以看到,首先判断本 HandleScope 是否创建了额外的内存块,是则释放,然后把 previous_ 中保存的上下文赋值给 current,那么 current 就恢复到了上一个 HandleScope 的上下文。,20230305203206b95ad6e952d6cf61689303e65c4f4b32b95936911,了解了 HandleScope 和 Handle 之后,我们接下来分析一下堆内存,主要是分析内存分配器、新生代和老生代(不包括代码区,map 区等)。,V8 堆内存的申请和回收由内存分配器管理,新生代和老生代只是负责使用。首先看看 MemoryAllocator 的定义。,MemoryAllocator 中由 chunks_ 负责管理具体的内存,chunks_ 是一个 ChunkInfo 链表,从 ChunkInfo 中可以看到它主要记录了内存的起始地址和大小,以及所属的 space(新生代、老生代)。,20230305203208f77dcea51ad7296ed898317740c09a1c036370896,接下来看 MemoryAllocator 的初始化。,主要是初始化了内部的一些数据结构,没有实质的操作。接下来才是分配内存。,接着看 ReserveInitialChunk。,这就完成了内存的申请。,进行了一系列的计算后,开始初始化新生代。接下来看 NewSpace。,新生代分为两个 SemiSpace。结构差不多,就不再介绍。了解了定义,看看初始化的逻辑。,至此,新生代的数据结构和初始化分析完成。接下来看老生代,老生代的数据结构和新生代差不多,不再分析,区别是老生代的内存是用链表管理的,而不是连续的内存,直接看初始化流程。,接着看 CommitPages。,继续看 InitializePagesInChunk。,一个内存块里面包括多个 Page,上面的代码是按 Page 为单位初始化内存。整体流程完成后,结构图如下。,20230305203208a6dd47226aa52c551670094f5121785c4dd9c9622,新建对象。,分析完数据结构和初始化流程,接下来看创建对象时的内存分配。,接着看 Allocate。,我们可以看到分配内存是非常快的,只需要移动一下指针。,但是有一个需要注意的是,当内存不够时就需要进行 GC,具体处理逻辑在之前代码的 CALL_HEAP_FUNCTION 宏中。,重点看 Heap::CollectGarbage。,早期的 V8 GC 是同步的,还没有并行、并发等优化逻辑,继续 PerformGarbageCollection。在 V8 初始化时会初始化新生代堆内存的数据结构。,这里我们可以看到经常听到的 Scavenge 和 MarkCompact 算法。首先看 Scavenge。,实际的逻辑比这个负责很多,不过这里只是大致了解流程。CopyVisitor 对象负责把对象复制到另一个区。,继续看 Heap::CopyObject。,MigrateObject 实现了对象的复制。,至此就完成了新生代 GC 的分析,紧接着分析老生代的。,老生代 GC 比较复杂。,这里我们只分析标记和清除,V8 早期也还没有并行标记,延迟清除等逻辑。,首先看如何从根开始遍历。,主要看 ImplementationUtilities::CurrentHandleScope()。,v8::HandleScope::current_ 就是前面介绍的 HandleScope。,Iterate 实现遍历全部的 Handle。接着看遍历时对对象的处理。具体实现在 MarkingVisitor。,扫描和标记完全部对象后就进行清除。,接着看 dealloc 的回收逻辑。,这里的内存并不是直接释放,而是存到空闲链表,后续使用。可参考前面的图。

© 版权声明

相关文章