前言:因为最近在做一些 gc track 的新现事情,所以打算了解一下 V8 GC 的生代实现。介绍 V8 GC 的垃圾文章网上已经有很多,就不打算再重复介绍。回收本文主要介绍一下新生代 GC 的新现实现,代码参考 V8 10.2,生代因为 GC 的垃圾实现非常复杂,只能介绍一些大致的回收实现,读者需要对 V8 GC 有一定的新现了解,比如新生代是生代分为 from 和 to 两个 space,然后在 GC 时是垃圾如何处理的。 说到 GC 首先需要介绍内存,回收具体来说,新现是生代堆内存,V8 把内存分为新生代和老生代,垃圾其中老生代又分为很多种类型,不过本文只关注新生代。下面先来看一下在 V8 初始化的过程中,涉及到新生代的部分,具体逻辑在 Heap::SetUpSpaces 函数。 void Heap::SetUpSpaces(...) { // 分配内存 space_[NEW_SPACE] = new_space_ = new NewSpace( this, memory_allocator_->data_page_allocator(), initial_semispace_size_, max_semi_space_size_, new_allocation_info); // 初始化 GC 调度对象 scavenge_job_.reset(new ScavengeJob()); scavenge_task_observer_.reset( new ScavengeTaskObserver( this, ScavengeJob::YoungGenerationTaskTriggerSize(this)) ); new_space()->AddAllocationObserver(scavenge_task_observer_.get()); // 初始化 GC 收集器 scavenger_collector_.reset(new ScavengerCollector(this)); 在 V8 的站群服务器堆中,通过 new_space_ 字段记录了新生代的堆内存对象,另外还有几个和 GC 相关的逻辑,scavenge_job_ 和 scavenge_task_observer_ 是处理 GC 对象,下面来逐个分析下。 1、 分配内存NewSpace::NewSpace(Heap* heap, v8::PageAllocator* page_allocator, size_t initial_semispace_capacity, size_t max_semispace_capacity, LinearAllocationArea* allocation_info) ..., to_space_(heap, kToSpace), from_space_(heap, kFromSpace) { to_space_.SetUp(initial_semispace_capacity, max_semispace_capacity); from_space_.SetUp(initial_semispace_capacity, max_semispace_capacity); to_space_.Commit(); NewSpace 中初始化了 from 和 to 两个 space。from 和 to 两个 space 是用 SemiSpace 表示。看一下它的 SetUp 方法。 void SemiSpace::SetUp(size_t initial_capacity, size_t maximum_capacity) { minimum_capacity_ = RoundDown(initial_capacity, Page::kPageSize); target_capacity_ = minimum_capacity_; maximum_capacity_ = RoundDown(maximum_capacity, Page::kPageSize); SetUp 初始化了该 space 的内存大小字段,但是还没有分配内存。SetUp 执行完之后接着调了 to space 的 Commit 的方法(没有调 from space 的 Commit 方法,根据 V8 的注释,因为 from space 是在 GC 时才需要的,这里大概是用了懒初始化)。接着看 Commit。 bool SemiSpace::Commit() { // 计算需要多少 Page const int num_pages = static_cast for (int pages_added = 0; pages_added < num_pages; pages_added++) { // 分配 Page Page* new_page = heap()->memory_allocator()->AllocatePage( MemoryAllocator::AllocationMode::kUsePool, this, NOT_EXECUTABLE); // 保存起来 memory_chunk_list_.PushBack(new_page); } Commit 根据需要的内存计算出 Page 数,然后分配内存,网站模板Page 是内存管理的单位,一块内存是由多个 Page 组成的。至此,新生代的内存分配完毕。 首先看一下 ScavengeJob。ScavengeJob 是管理 GC 调度的。 class ScavengeJob { public: ScavengeJob() V8_NOEXCEPT = default; // 判断是否需要发起 GC void ScheduleTaskIfNeeded(Heap* heap); // 发起 GC 的阈值 static size_t YoungGenerationTaskTriggerSize(Heap* heap); private: class Task; // 判断内存是否达到了阈值 static bool YoungGenerationSizeTaskTriggerReached(Heap* heap); void set_task_pending(bool value) { task_pending_ = value; } bool task_pending_ = false; ScavengeJob 记录了内存达到多少时需要发起 GC,并实现了发起 GC 的逻辑。我们先看一下阈值。 size_t ScavengeJob::YoungGenerationTaskTriggerSize(Heap* heap) { // FLAG_scavenge_task_trigger = 80 return heap->new_space()->Capacity() * FLAG_scavenge_task_trigger / 100; } bool ScavengeJob::YoungGenerationSizeTaskTriggerReached(Heap* heap) { return heap->new_space()->Size() >= YoungGenerationTaskTriggerSize(heap); V8 默认逻辑是内存达到 80% 时触发 GC,可以通过 scavenge_task_trigger flag 进行控制。V8 会调用 ScheduleTaskIfNeeded 判断是否需要发起 GC。 void ScavengeJob::ScheduleTaskIfNeeded(Heap* heap) { if (FLAG_scavenge_task && !task_pending_ && !heap->IsTearingDown() && YoungGenerationSizeTaskTriggerReached(heap)) { v8::Isolate* isolate = reinterpret_cast auto taskrunner = V8::GetCurrentPlatform()->GetForegroundTaskRunner(isolate); if (taskrunner->NonNestableTasksEnabled()) { taskrunner->PostNonNestableTask( std::make_unique ); task_pending_ = true; } } } ScheduleTaskIfNeeded 首先判断内存是否达到了阈值,是的就给线程池提交一个 GC 人物。V8 中有一个 platform 的概念,服务器租用比如在 Node.js 里是 NodePlatform,这个对象内部有一个线程池,V8 会把 GC 任务提交到线程池中等待处理。一个 GC 任务由 Task 对象表示。 class ScavengeJob::Task : public CancelableTask { public: Task(Isolate* isolate, ScavengeJob* job) : CancelableTask(isolate), isolate_(isolate), job_(job) { } // CancelableTask overrides. void RunInternal() override; Isolate* isolate() const { return isolate_; } private: Isolate* const isolate_; ScavengeJob* const job_; Task 继承了 CancelableTask,并且内部有一个 ScavengeJob 对象。 class V8_EXPORT_PRIVATE CancelableTask : public Cancelable, NON_EXPORTED_BASE(public Task) { public: // Task overrides. void Run() final { if (TryRun()) { RunInternal(); } } virtual void RunInternal() = 0; 当任务给线程池调度执行时,CancelableTask 的 Run 函数会被执行,从而执行 RunInternal 函数,该函数由子类实现。接着看 ScavengeJob::Task 中关于这个函数的实现。 void ScavengeJob::Task::RunInternal() { VMState if (ScavengeJob::YoungGenerationSizeTaskTriggerReached(isolate()->heap())) { isolate()->heap()->CollectGarbage(NEW_SPACE, GarbageCollectionReason::kTask); } job_->set_task_pending(false); 这里再次进行了内存是否达到阈值的判断,如果达到了就直接进行 GC,下面看 CollectGarbage。 bool Heap::CollectGarbage(AllocationSpace space, GarbageCollectionReason gc_reason, const v8::GCCallbackFlags gc_callback_flags) { const char* collector_reason = nullptr; // 根据 space 选择 GC 回收器类型,这里是新生代,选择的是 SCAVENGER,具体可以参考 SelectGarbageCollector 逻辑 GarbageCollector collector = SelectGarbageCollector(space, &collector_reason); // 根据 GC 回收器类型选择 GC 类型,这个就是我们在 gc track 时拿到的类型 GCType gc_type = GetGCTypeFromGarbageCollector(collector); { GCCallbacksScope scope(this); // 执行“开始 GC” 回调,我们注册的 GC track 回调在这里被执行 if (scope.CheckReenter()) { CallGCPrologueCallbacks(gc_type, kNoGCCallbackFlags); } } // 执行 GC PerformGarbageCollection(collector, gc_reason, collector_reason, gc_callback_flags); // 执行 “GC 执行完”回调 { GCCallbacksScope scope(this); if (scope.CheckReenter()) { CallGCEpilogueCallbacks(gc_type, gc_callback_flags); } } } 接着看 PerformGarbageCollection。 size_t Heap::PerformGarbageCollection( GarbageCollector collector, GarbageCollectionReason gc_reason, const char* collector_reason, const v8::GCCallbackFlags gc_callback_flags) { switch (collector) { case GarbageCollector::MARK_COMPACTOR: MarkCompact(); break; case GarbageCollector::MINOR_MARK_COMPACTOR: MinorMarkCompact(); break; case GarbageCollector::SCAVENGER: Scavenge(); break; } 继续调用 Scavenge。 void Heap::Scavenge() { // 进行 from space 和 to space 的翻转 new_space()->Flip(); new_space()->ResetLinearAllocationArea(); // We also flip the young generation large object space. All large objects // will be in the from space. new_lo_space()->Flip(); new_lo_space()->ResetPendingObject(); // Implements Cheneys copying algorithm scavenger_collector_->CollectGarbage(); } Scavenge 是真正执行 GC 的地方,首先第一步进行 from space 和 to space 的翻转,然后执行 GC。我们看看翻转的逻辑。 void NewSpace::Flip() { SemiSpace::Swap(&from_space_, &to_space_); } void SemiSpace::Swap(SemiSpace* from, SemiSpace* to) { auto saved_to_space_flags = to->current_page()->GetFlags(); // We swap all properties but id_. std::swap(from->target_capacity_, to->target_capacity_); std::swap(from->maximum_capacity_, to->maximum_capacity_); std::swap(from->minimum_capacity_, to->minimum_capacity_); std::swap(from->age_mark_, to->age_mark_); std::swap(from->memory_chunk_list_, to->memory_chunk_list_); std::swap(from->current_page_, to->current_page_); std::swap(from->external_backing_store_bytes_, to->external_backing_store_bytes_); std::swap(from->committed_physical_memory_, to->committed_physical_memory_); to->FixPagesFlags(saved_to_space_flags, Page::kCopyOnFlipFlagsMask); from->FixPagesFlags(Page::NO_FLAGS, Page::NO_FLAGS); 这里只是进行了一些字段的交换,真正的逻辑在 GC 收集器中。 void ScavengerCollector::CollectGarbage() { ScopedFullHeapCrashKey collect_full_heap_dump_if_crash(isolate_); std::vector Scavenger::EmptyChunksList empty_chunks; // 计算需要提交多少个 GC 任务 const int num_scavenge_tasks = NumberOfScavengeTasks(); Scavenger::CopiedList copied_list; Scavenger::PromotionList promotion_list; EphemeronTableList ephemeron_table_list; { for (int i = 0; i < num_scavenge_tasks; ++i) { scavengers.emplace_back( new Scavenger(this, heap_, is_logging, &empty_chunks, &copied_list, &promotion_list, &ephemeron_table_list, i)); } // 拿到 heap 中的所有内存块 std::vector heap_, [&memory_chunks](MemoryChunk* chunk) { memory_chunks.emplace_back(ParallelWorkItem{ }, chunk); }); // 遍历堆对象迭代器 RootScavengeVisitor root_scavenge_visitor(scavengers[kMainThreadId].get()); { // 遍历堆对象 heap_->IterateRoots(&root_scavenge_visitor, options); // 遍历 global handle 对象 isolate_->global_handles()->IterateYoungStrongAndDependentRoots( &root_scavenge_visitor); scavengers[kMainThreadId]->Publish(); } // 提交 GC 任务 { // Parallel phase scavenging all copied and promoted objects. V8::GetCurrentPlatform() ->PostJob(v8::TaskPriority::kUserBlocking, std::make_unique std::move(memory_chunks), &copied_list, &promotion_list)) ->Join(); } } // 回收 ArrayBuffer 内存,比如 Node.js 的 Buffer { TRACE_GC(heap_->tracer(), GCTracer::Scope::SCAVENGER_SWEEP_ARRAY_BUFFERS); SweepArrayBufferExtensions(); } 这里的逻辑非常多,除了回收新生代对象的内存,还会处理 global handle 和 ArrayBuffer 的内存。不过这里我们只关注一般的新生代对象。接着遍历堆对象的过程。 void Heap::IterateRoots(RootVisitor* v, base::EnumSet v->VisitRootPointers(Root::kStrongRootList, nullptr, roots_table().strong_roots_begin(), roots_table().strong_roots_end()); // ... 省略非常多逻辑 } Heap 对象提供了迭代的接口,具体迭代逻辑由 Visitor 实现,这里是 RootScavengeVisitor。 void RootScavengeVisitor::VisitRootPointer(Root root, const char* description, FullObjectSlot p) { ScavengePointer(p); } void RootScavengeVisitor::ScavengePointer(FullObjectSlot p) { Object object = *p; // 如果是新生代对象则处理 if (Heap::InYoungGeneration(object)) { scavenger_->ScavengeObject(FullHeapObjectSlot(p), HeapObject::cast(object)); } 接着看 ScavengeObject。 template SlotCallbackResult Scavenger::ScavengeObject(THeapObjectSlot p, HeapObject object) { return EvacuateObject(p, map, object); } template SlotCallbackResult Scavenger::EvacuateObject(THeapObjectSlot slot, Map map, HeapObject source) { int size = source.SizeFromMap(map); // 堆对象的大小 VisitorId visitor_id = map.visitor_id(); switch (visitor_id) { // ... 省略其他 case default: return EvacuateObjectDefault(map, slot, source, size, Map::ObjectFieldsFrom(visitor_id)); } } emplate SlotCallbackResult Scavenger::EvacuateObjectDefault( Map map, THeapObjectSlot slot, HeapObject object, int object_size, ObjectFields object_fields) { // 是否可以晋升到老生代,新生代对象经过 n 次 GC 还存活则可以晋升到老生代(n = 1) if (!heap()->ShouldBePromoted(object.address())) { // 不能晋升则移到 from space result = SemiSpaceCopyObject(map, slot, object, object_size, object_fields); } // 否则晋升到老生代 result = PromoteObject map, slot, object, object_size, object_fields); // 晋升失败则 fallback,移到 from 区 SemiSpaceCopyObject(map, slot, object, object_size, object_fields); 接着看 SemiSpaceCopyObject 和 PromoteObject。 template CopyAndForwardResult Scavenger::SemiSpaceCopyObject( Map map, THeapObjectSlot slot, HeapObject object, int object_size, ObjectFields object_fields) { AllocationAlignment alignment = HeapObject::RequiredAlignment(map); // 在 from space 分配一块新的内存,把 to space 的对象移动过去 AllocationResult allocation = allocator_.Allocate( NEW_SPACE, object_size, AllocationOrigin::kGC, alignment); // 进行对象迁移 MigrateObject(map, object, target, object_size, kPromoteIntoLocalHeap); } bool Scavenger::MigrateObject(Map map, HeapObject source, HeapObject target, int size, PromotionHeapChoice promotion_heap_choice) { // 内存复制,比如通过 memmove heap()->CopyBlock(target.address() + kTaggedSize, source.address() + kTaggedSize, size - kTaggedSize); // 触发对象移动事件,比如 heap_profiler 回监听这个事件 if (V8_UNLIKELY(is_logging_)) { heap()->OnMoveEvent(target, source, size); } 至此就完成了对象的迁移。接着看对象的晋升。 template Scavenger::PromotionHeapChoice promotion_heap_choice> CopyAndForwardResult Scavenger::PromoteObject(Map map, THeapObjectSlot slot, HeapObject object, int object_size, ObjectFields object_fields) { AllocationAlignment alignment = HeapObject::RequiredAlignment(map); AllocationResult allocation; // 在老生代分配一块内存 allocation = allocator_.Allocate(OLD_SPACE, object_size, AllocationOrigin::kGC, alignment); HeapObject target; allocation.To(&target); // 迁移过去 MigrateObject(map, object, target, object_size, promotion_heap_choice); 对象的晋升本质上也是内存的复制,只不过是复制到了老生代的内存。完成了 from space 和 to space 对象的处理后,还需要另外的任务需要处理。具体由提交给线程池的 JobTask 对象实现。 V8::GetCurrentPlatform() ->PostJob(v8::TaskPriority::kUserBlocking, std::make_unique std::move(memory_chunks), &copied_list, &promotion_list)) ->Join(); 来看一下该对象 Run 的实现。 void ScavengerCollector::JobTask::Run(JobDelegate* delegate) { Scavenger* scavenger = (*scavengers_)[delegate->GetTaskId()].get(); ProcessItems(delegate, scavenger); } void ScavengerCollector::JobTask::ProcessItems(JobDelegate* delegate, Scavenger* scavenger) { double scavenging_time = 0.0; { TimedScope scope(&scavenging_time); // 并行处理内存 ConcurrentScavengePages(scavenger); scavenger->Process(delegate); } } void ScavengerCollector::JobTask::ConcurrentScavengePages( Scavenger* scavenger) { while (remaining_memory_chunks_.load(std::memory_order_relaxed) > 0) { base::Optional if (!index) return; for (size_t i = *index; i < memory_chunks_.size(); ++i) { auto& work_item = memory_chunks_[i]; if (!work_item.first.TryAcquire()) break; scavenger->ScavengePage(work_item.second); if (remaining_memory_chunks_.fetch_sub(1, std::memory_order_relaxed) <= 1) { return; } } } } 具体看 scavenger->ScavengePage(work_item.second) 。 void Scavenger::ScavengePage(MemoryChunk* page) { CodePageMemoryModificationScope memory_modification_scope(page); if (page->slot_set InvalidatedSlotsFilter filter = InvalidatedSlotsFilter::OldToNew(page); RememberedSet page, [this, &filter](MaybeObjectSlot slot) { if (!filter.IsValid(slot.address())) return REMOVE_SLOT; return CheckAndScavengeObject(heap_, slot); }, &empty_chunks_local_); } if (page->invalidated_slots page->ReleaseInvalidatedSlots } RememberedSet page, [=](SlotType type, Address addr) { return UpdateTypedSlotHelper::UpdateTypedSlot( heap_, type, addr, [this](FullMaybeObjectSlot slot) { return CheckAndScavengeObject(heap(), slot); }); }); AddPageToSweeperIfNecessary(page); } 大概就是进行了数据的更新和内存的回收。至此,GC 的流程就大致分析完了。 刚出分析了 GC 的处理过程,接下来看看什么时候会触发 GC。相关代码如下。 scavenge_task_observer_.reset(new ScavengeTaskObserver(this, ScavengeJob::YoungGenerationTaskTriggerSize(this))); V8 初始化时,给新生代对象注册了一个内存分配的观察者,首先看一下观察者的实现。 class ScavengeTaskObserver : public AllocationObserver { public: ScavengeTaskObserver(Heap* heap, intptr_t step_size) : AllocationObserver(step_size), heap_(heap) { } void Step(int bytes_allocated, Address, size_t) override { heap_->ScheduleScavengeTaskIfNeeded(); } private: Heap* heap_; 观察者的实现很简单,V8 在分配内存的过程中会执行观察者的 Step 方法,该方法会判断是否需要 GC,下面是 ScheduleScavengeTaskIfNeeded 的实现。 void Heap::ScheduleScavengeTaskIfNeeded() { scavenge_job_->ScheduleTaskIfNeeded(this); } void ScavengeJob::ScheduleTaskIfNeeded(Heap* heap) { if (FLAG_scavenge_task && !task_pending_ && !heap->IsTearingDown() && YoungGenerationSizeTaskTriggerReached(heap)) { v8::Isolate* isolate = reinterpret_cast auto taskrunner = V8::GetCurrentPlatform()->GetForegroundTaskRunner(isolate); if (taskrunner->NonNestableTasksEnabled()) { taskrunner->PostNonNestableTask( std::make_unique task_pending_ = true; } } 这个过程刚出已经分析过了。接下来再往前看,什么时候会调用 Step。创建完观察者后,会把观察者注册到 newSpace 中。 看一下 AddAllocationObserver。 void Space::AddAllocationObserver(AllocationObserver* observer) { allocation_counter_.AddAllocationObserver(observer); 那么 allocation_counter_ 又是什么呢?allocation_counter_ 是 AllocationCounter 对象。 class AllocationCounter final { public: AllocationCounter() = default; V8_EXPORT_PRIVATE void AddAllocationObserver(AllocationObserver* observer); V8_EXPORT_PRIVATE void RemoveAllocationObserver(AllocationObserver* observer); V8_EXPORT_PRIVATE void AdvanceAllocationObservers(size_t allocated); V8_EXPORT_PRIVATE void InvokeAllocationObservers(Address soon_object, size_t object_size, size_t aligned_object_size); private: struct AllocationObserverCounter final { AllocationObserverCounter(AllocationObserver* observer, size_t prev_counter, size_t next_counter) : observer_(observer), prev_counter_(prev_counter), next_counter_(next_counter) { } AllocationObserver* observer_; }; std::vector observers_; }; AllocationCounter 里记录了多个 AllocationObserverCounter 对象,而 AllocationObserverCounter 对象封装了 AllocationObserver 对象。来看一下 AddAllocationObserver 方法的实现。 void AllocationCounter::AddAllocationObserver(AllocationObserver* observer) { observers_.push_back(AllocationObserverCounter(observer, current_counter_, observer_next_counter)); newSpace 通过 AllocationCounter 管理了多个观察者,接着看调用观察者的时机,也就是分配内存的时候。 AllocationResult SpaceWithLinearArea::AllocateRawAligned( int size_in_bytes, AllocationAlignment alignment, AllocationOrigin origin) { // 分配内存 AllocationResult result = AllocateFastAligned( size_in_bytes, &aligned_size_in_bytes, alignment, origin); // 调用观察者 InvokeAllocationObservers(result.ToAddress(), size_in_bytes, aligned_size_in_bytes, max_aligned_size); return result; } 接着看 InvokeAllocationObservers。 void AllocationCounter::InvokeAllocationObservers(Address soon_object, size_t object_size, size_t aligned_object_size) { for (AllocationObserverCounter& aoc : observers_) { if (aoc.next_counter_ - current_counter_ <= aligned_object_size) { { // 执行观察者的 Step 方法,也就是刚出分析 GC 处理时提到的 aoc.observer_->Step( static_cast object_size); } } } 至此,所有的过程分析完毕。 V8 的 GC 经过多年的优化已经变得非常高效,和其他优化技术一起实现了 V8 引擎的高性能。具体的实现非常复杂,涉及的逻辑非常多,时间有限,也就只能大致分析一下,了解基础的原理。