当前位置:首页 > 应用开发

V8 新生代垃圾回收的实现

前言:因为最近在做一些 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(target_capacity_ / Page::kPageSize);

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 组成的。至此,新生代的内存分配完毕。

2、 GC 处理

首先看一下 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(heap->isolate());

auto taskrunner = V8::GetCurrentPlatform()->GetForegroundTaskRunner(isolate);

if (taskrunner->NonNestableTasksEnabled()) {

taskrunner->PostNonNestableTask(

std::make_unique(heap->isolate(), this)

);

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() {

VMStatestate(isolate());

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> scavengers;

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> memory_chunks;RememberedSet::IterateMemoryChunks(

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(this, &scavengers,

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::EnumSetoptions) {

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(this, &scavengers,

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::Optionalindex = generator_.GetNext();

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() != nullptr) {

InvalidatedSlotsFilter filter = InvalidatedSlotsFilter::OldToNew(page);

RememberedSet::IterateAndTrackEmptyBuckets(

page,

[this, &filter](MaybeObjectSlot slot) {

if (!filter.IsValid(slot.address())) return REMOVE_SLOT;

return CheckAndScavengeObject(heap_, slot);

},

&empty_chunks_local_);

}

if (page->invalidated_slots() != nullptr) {

page->ReleaseInvalidatedSlots();

}

RememberedSet::IterateTyped(

page, [=](SlotType type, Address addr) {

return UpdateTypedSlotHelper::UpdateTypedSlot(

heap_, type, addr, [this](FullMaybeObjectSlot slot) {

return CheckAndScavengeObject(heap(), slot);

});

});

AddPageToSweeperIfNecessary(page);

}

大概就是进行了数据的更新和内存的回收。至此,GC 的流程就大致分析完了。

触发 GC

刚出分析了 GC 的处理过程,接下来看看什么时候会触发 GC。相关代码如下。

scavenge_task_observer_.reset(new ScavengeTaskObserver(this, ScavengeJob::YoungGenerationTaskTriggerSize(this)));

new_space()->AddAllocationObserver(scavenge_task_observer_.get());

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(heap->isolate());

auto taskrunner =

V8::GetCurrentPlatform()->GetForegroundTaskRunner(isolate);

if (taskrunner->NonNestableTasksEnabled()) {

taskrunner->PostNonNestableTask(

std::make_unique(heap->isolate(), this));

task_pending_ = true;

}

}

}

这个过程刚出已经分析过了。接下来再往前看,什么时候会调用 Step。创建完观察者后,会把观察者注册到 newSpace 中。

new_space()->AddAllocationObserver(scavenge_task_observer_.get());

看一下 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(current_counter_ - aoc.prev_counter_), soon_object,

object_size);

}

}

}

}

至此,所有的过程分析完毕。

4 、总结

V8 的 GC 经过多年的优化已经变得非常高效,和其他优化技术一起实现了 V8 引擎的高性能。具体的实现非常复杂,涉及的逻辑非常多,时间有限,也就只能大致分析一下,了解基础的原理。

分享到:

滇ICP备2023006006号-16