前言:因为最近在做一些 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);
scavenge_job_.reset(new ScavengeJob());
scavenge_task_observer_.reset(
new ScavengeTaskObserver(
this,
ScavengeJob::YoungGenerationTaskTriggerSize(this))
);
new_space()->AddAllocationObserver(scavenge_task_observer_.get());
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() {
const int num_pages = static_cast<int>(target_capacity_ / Page::kPageSize);
for (int pages_added = 0; pages_added < num_pages; pages_added++) {
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;
void ScheduleTaskIfNeeded(Heap* heap);
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) {
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<v8::Isolate*>(heap->isolate());
auto taskrunner = V8::GetCurrentPlatform()->GetForegroundTaskRunner(isolate);
if (taskrunner->NonNestableTasksEnabled()) {
taskrunner->PostNonNestableTask(
std::make_unique<Task>(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) {}
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:
void Run() final {
if (TryRun()) {
RunInternal();
}
}
virtual void RunInternal() = 0;
};
当任务给线程池调度执行时,CancelableTask 的 Run 函数会被执行,从而执行 RunInternal 函数,该函数由子类实现。接着看 ScavengeJob::Task 中关于这个函数的实现。
void ScavengeJob::Task::RunInternal() {
VMState<GC> state(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;
GarbageCollector collector = SelectGarbageCollector(space, &collector_reason);
GCType gc_type = GetGCTypeFromGarbageCollector(collector);
{
GCCallbacksScope scope(this);
if (scope.CheckReenter()) {
CallGCPrologueCallbacks(gc_type, kNoGCCallbackFlags);
}
}
PerformGarbageCollection(collector, gc_reason, collector_reason, gc_callback_flags);
{
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() {
new_space()->Flip();
new_space()->ResetLinearAllocationArea();
new_lo_space()->Flip();
new_lo_space()->ResetPendingObject();
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();
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<std::unique_ptr<Scavenger>> scavengers;
Scavenger::EmptyChunksList empty_chunks;
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));
}
std::vector<std::pair<ParallelWorkItem, MemoryChunk*>> memory_chunks;
RememberedSet<OLD_TO_NEW>::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);
isolate_->global_handles()->IterateYoungStrongAndDependentRoots( &root_scavenge_visitor);
scavengers[kMainThreadId]->Publish();
}
{
V8::GetCurrentPlatform()
->PostJob(v8::TaskPriority::kUserBlocking,
std::make_unique<JobTask>(this, &scavengers,
std::move(memory_chunks),
&copied_list, &promotion_list))
->Join();
}
}
{
TRACE_GC(heap_->tracer(), GCTracer::Scope::SCAVENGER_SWEEP_ARRAY_BUFFERS);
SweepArrayBufferExtensions();
}
}
这里的逻辑非常多,除了回收新生代对象的内存,还会处理 global handle 和 ArrayBuffer 的内存。不过这里我们只关注一般的新生代对象。接着遍历堆对象的过程。
void Heap::IterateRoots(RootVisitor* v, base::EnumSet<SkipRoot> options) {
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 <typename THeapObjectSlot>
SlotCallbackResult Scavenger::ScavengeObject(THeapObjectSlot p,
HeapObject object) {
return EvacuateObject(p, map, object);
}
template <typename THeapObjectSlot>
SlotCallbackResult Scavenger::EvacuateObject(THeapObjectSlot slot, Map map,
HeapObject source) {
int size = source.SizeFromMap(map);
VisitorId visitor_id = map.visitor_id();
switch (visitor_id) {
default:
return EvacuateObjectDefault(map, slot, source, size,
Map::ObjectFieldsFrom(visitor_id));
}
}
emplate <typename THeapObjectSlot, Scavenger::PromotionHeapChoice promotion_heap_choice>
SlotCallbackResult Scavenger::EvacuateObjectDefault(
Map map, THeapObjectSlot slot, HeapObject object, int object_size,
ObjectFields object_fields) {
if (!heap()->ShouldBePromoted(object.address())) {
result = SemiSpaceCopyObject(map, slot, object, object_size, object_fields);
}
result = PromoteObject<THeapObjectSlot, promotion_heap_choice>(
map, slot, object, object_size, object_fields);
SemiSpaceCopyObject(map, slot, object, object_size, object_fields);
}
接着看 SemiSpaceCopyObject 和 PromoteObject。
template <typename THeapObjectSlot>
CopyAndForwardResult Scavenger::SemiSpaceCopyObject(
Map map, THeapObjectSlot slot, HeapObject object, int object_size,
ObjectFields object_fields) {
AllocationAlignment alignment = HeapObject::RequiredAlignment(map);
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) {
heap()->CopyBlock(target.address() + kTaggedSize,
source.address() + kTaggedSize, size - kTaggedSize);
if (V8_UNLIKELY(is_logging_)) {
heap()->OnMoveEvent(target, source, size);
}
}
至此就完成了对象的迁移。接着看对象的晋升。
template <typename THeapObjectSlot,
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<JobTask>(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::Optional<size_t> index = 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<OLD_TO_NEW, AccessMode::ATOMIC>() != nullptr) {
InvalidatedSlotsFilter filter = InvalidatedSlotsFilter::OldToNew(page);
RememberedSet<OLD_TO_NEW>::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<OLD_TO_NEW>() != nullptr) {
page->ReleaseInvalidatedSlots<OLD_TO_NEW>();
}
RememberedSet<OLD_TO_NEW>::IterateTyped(
page, [=](SlotType type, Address addr) {
return UpdateTypedSlotHelper::UpdateTypedSlot(
heap_, type, addr, [this](FullMaybeObjectSlot slot) {
return CheckAndScavengeObject(heap(), slot);
});
});
AddPageToSweeperIfNecessary(page);
}
大概就是进行了数据的更新和内存的回收。至此,GC 的流程就大致分析完了。
3 触发 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<v8::Isolate*>(heap->isolate());
auto taskrunner =
V8::GetCurrentPlatform()->GetForegroundTaskRunner(isolate);
if (taskrunner->NonNestableTasksEnabled()) {
taskrunner->PostNonNestableTask(
std::make_unique<Task>(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<AllocationObserverCounter> 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) {
{
aoc.observer_->Step(
static_cast<int>(current_counter_ - aoc.prev_counter_), soon_object,
object_size);
}
}
}
}
至此,所有的过程分析完毕。
4 总结
V8 的 GC 经过多年的优化已经变得非常高效,和其他优化技术一起实现了 V8 引擎的高性能。具体的实现非常复杂,涉及的逻辑非常多,时间有限,也就只能大致分析一下,了解基础的原理。
|