V8 除了我们经常讲到的堆的实新生代和老生代的常规堆内存外,还有另一种堆内存,外内就是垃圾堆外内存。堆外内存本质上也是回收堆内存,只不过不是堆的实由 V8 进行分配,而是外内由 V8 的调用方分配,比如 Node.js,垃圾但是回收是由 V8 负责 GC 的。本文介绍堆外内存的堆的实一种类型 ArrayBuffer 的 GC 实现。 ArrayBuffer 的外内创建有很多种方式,比如在 JS 层创建 Uint8Array 或者 ArrayBuffer(对应实现 builtins-arraybuffer.cc),垃圾又比如自己在 C++ 层调用 V8 提供的回收 API 进行创建,它们最终对应的堆的实实现是一样的。站群服务器为了简单起见,外内这里以通过 V8 API 创建的垃圾方式进行分析。对应头文件是 v8-array-buffer.h 的 ArrayBuffer。创建方式有很多种,这里以最简单的方式进行分析。 static Local New(Isolate* isolate, size_t byte_length); 通过调用 ArrayBuffer::New 就可以创建一个 ArrayBuffer 对象。来看看具体实现。 Local v8::ArrayBuffer::New(Isolate* isolate, size_t byte_length) { i::Isolate* i_isolate = reinterpret_cast i::MaybeHandle i_isolate->factory()->NewJSArrayBufferAndBackingStore( byte_length, i::InitializedFlag::kZeroInitialized); i::Handle if (!result.ToHandle(&array_buffer)) { // ... } return Utils::ToLocal(array_buffer); } 首先看 NewJSArrayBufferAndBackingStore。 MaybeHandle size_t byte_length, InitializedFlag initialized, AllocationType allocation) { std::unique_ptr if (byte_length > 0) { // 分配一块内存 backing_store = BackingStore::Allocate(isolate(), byte_length, SharedFlag::kNotShared, initialized); } // map 标记对象的类型 Handle isolate()); // 新建一个 JSArrayBuffer 对象,默认在新生代申请内存 auto array_buffer = Handle // 关联 JSArrayBuffer 和 内存 array_buffer->Setup(SharedFlag::kNotShared, ResizableFlag::kNotResizable, std::move(backing_store)); return array_buffer; } NewJSArrayBufferAndBackingStore 的逻辑非常多,每一步都是需要了解的,我们逐句分析。 std::unique_ptr Isolate* isolate, size_t byte_length, SharedFlag shared, InitializedFlag initialized) { void* buffer_start = nullptr; // 获取 array_buffer 内存分配器,由 V8 调用方提供 auto allocator = isolate->array_buffer_allocator(); if (byte_length != 0) { auto allocate_buffer = [allocator, initialized](size_t byte_length) { if (initialized == InitializedFlag::kUninitialized) { return allocator->AllocateUninitialized(byte_length); } void* buffer_start = allocator->Allocate(byte_length); return buffer_start; }; // 执行 allocate_buffer 函数分配内存 buffer_start = isolate->heap()->AllocateExternalBackingStore( allocate_buffer, byte_length); } // 交给 BackingStore 管理 auto result = new BackingStore(buffer_start, // start byte_length, // length byte_length, // max length byte_length, // capacity shared, // shared ResizableFlag::kNotResizable, // resizable false, // is_wasm_memory true, // free_on_destruct false, // has_guard_regions false, // custom_deleter false); // empty_deleter // 设置一些上下文,销毁内存是用 / void BackingStore::SetAllocatorFromIsolate(Isolate* isolate) { type_specific_data_.v8_api_array_buffer_allocator = isolate->array_buffer_allocator(); } */ result->SetAllocatorFromIsolate(isolate); return std::unique_ptr } 首先获取 array_buffer_allocator 内存分配器,该分配器由 V8 的调用方提供,比如 Node.js 的 NodeArrayBufferAllocator。然后通过该分配器分配内存,通常是通过 calloc,malloc 等函数分配内存。不过这里不是云服务器提供商直接分配,而是通过封装一个函数交给 AllocateExternalBackingStore 函数进行处理。 void* Heap::AllocateExternalBackingStore( const std::function // 执行函数分配内存 void* result = allocate(byte_length); // 成功则返回 if (result) return result; // 失败则进行 GC 后再次执行 if (!always_allocate()) { for (int i = 0; i < 2; i++) { CollectGarbage(OLD_SPACE, GarbageCollectionReason::kExternalMemoryPressure); result = allocate(byte_length); if (result) return result; } isolate()->counters()->gc_last_resort_from_handles()->Increment(); CollectAllAvailableGarbage( GarbageCollectionReason::kExternalMemoryPressure); } return allocate(byte_length); } AllocateExternalBackingStore 主要是为了在分配内存失败时,进行 GC 尝试腾出一些内存。分配完内存后,就把这块内存交给 BackingStore 管理。BackingStore 就不进行分析了,主要是记录了内存的一些信息,比如开始和结束地址。拿到一块内存后就会创建一个 JSArrayBuffer 对象进行关联。JSArrayBuffer 是 ArrayBuffer 在 V8 中的具体实现。接着看。 NewJSObjectFromMap 根据 map 在 allocation 指示的地方分配一个内存用来存储 JSArrayBuffer 对象。map 表示对象的类型,allocation 表示在哪个 space 分配这块内存,默认是新生代。来看下 NewJSObjectFromMap。 Handle Handle Handle allocation_site) { JSObject js_obj = JSObject::cast(AllocateRawWithAllocationSite(map, allocation, allocation_site)); InitializeJSObjectFromMap(js_obj, *empty_fixed_array(), *map); return handle(js_obj, isolate()); } AllocateRawWithAllocationSite 最终调用 allocator()->AllocateRawWith 在新生代分配了一块内存,大小是一个 JSArrayBuffer 的内存,因为 JSArrayBuffer 是 JSObject 的子类,网站模板所以上面可以转成 JSObject 进行一些操作,完成之后我们就拿到了一个 JSArrayBuffer 对象。接着看最后一步。 Setup 是把申请的 BackingStore 对象和 JSArrayBuffer 对象关联起来,JSArrayBuffer 对象不涉及存储数据的内存,它只是保存了一些元信息,比如内存大小。具体存储数据的内存由 BackingStore 管理。看看 Setup 的实现。 void JSArrayBuffer::Setup(SharedFlag shared, ResizableFlag resizable, std::shared_ptr clear_padding(); set_bit_field(0); set_is_shared(shared == SharedFlag::kShared); set_is_resizable(resizable == ResizableFlag::kResizable); set_is_detachable(shared != SharedFlag::kShared); for (int i = 0; i < v8::ArrayBuffer::kEmbedderFieldCount; i++) { SetEmbedderField(i, Smi::zero()); } set_extension(nullptr); Attach(std::move(backing_store)); 做了一些初始化处理,然后调用 Attach。 void JSArrayBuffer::Attach(std::shared_ptr Isolate* isolate = GetIsolate(); set_backing_store(isolate, backing_store->buffer_start()); set_byte_length(backing_store->byte_length()); set_max_byte_length(backing_store->max_byte_length()); // 创建 ArrayBufferExtension 对象 ArrayBufferExtension* extension = EnsureExtension(); // 内存大小 size_t bytes = backing_store->PerIsolateAccountingLength(); // 关联起来 extension->set_accounting_length(bytes); extension->set_backing_store(std::move(backing_store)); // 注册到管理 GC 的对象中 isolate->heap()->AppendArrayBufferExtension(*this, extension); } Attach 是最重要的逻辑,首先把 BackingStore 对象保存到 JSArrayBuffer 对象中,然后通过 EnsureExtension 创建了一个 ArrayBufferExtension 对象,ArrayBufferExtension 是为了 GC 管理。 ArrayBufferExtension* JSArrayBuffer::EnsureExtension() { ArrayBufferExtension* extension = this->extension(); if (extension != nullptr) return extension; extension = new ArrayBufferExtension(std::shared_ptr set_extension(extension); return extension; ArrayBufferExtension 对象保存了内存的大小和其管理对象 BackingStore。最终形成的关系如下。 对象关联完毕后,通过 isolate->heap()->AppendArrayBufferExtension(*this, extension); 把 ArrayBufferExtension 对象注册到负责管理 GC 的对象中。 void Heap::AppendArrayBufferExtension(JSArrayBuffer object, ArrayBufferExtension* extension) { array_buffer_sweeper_->Append(object, extension); } array_buffer_sweeper_ 是 ArrayBufferSweeper 对象,该对象在 V8 初始化时创建,看一下它的 Append 函数。 void ArrayBufferSweeper::Append(JSArrayBuffer object, ArrayBufferExtension* extension) { size_t bytes = extension->accounting_length(); if (Heap::InYoungGeneration(object)) { young_.Append(extension); } else { old_.Append(extension); } // 通知 V8 堆外内存的大小增加 bytes 字节 IncrementExternalMemoryCounters(bytes); ArrayBufferSweeper 维护了新生代和老生代两个队列,根据 JSArrayBuffer 对象在哪个 space 来决定插入哪个队列,刚出分析过,JSArrayBuffer 默认在新生代创建。 void ArrayBufferList::Append(ArrayBufferExtension* extension) { if (head_ == nullptr) { head_ = tail_ = extension; } else { tail_->set_next(extension); tail_ = extension; } const size_t accounting_length = extension->accounting_length(); bytes_ += accounting_length; extension->set_next(nullptr); } Append 就是把对象插入队列,并且更新已经分配的内存大小。这样就完成了一个 ArrayBuffer 对象的创建。 接着看 GC 的逻辑,具体在 RequestSweep 函数,该函数在几个地方被调用,比如新生代进行 GC 时。 void ScavengerCollector::SweepArrayBufferExtensions() { heap_->array_buffer_sweeper()->RequestSweep( ArrayBufferSweeper::SweepingType::kYoung); } 看一下这个函数的功能。 void ArrayBufferSweeper::RequestSweep(SweepingType type) { if (young_.IsEmpty() && (old_.IsEmpty() || type == SweepingType::kYoung)) return; // 做一些准备工作 Prepare(type); auto task = MakeCancelableTask(heap_->isolate(), [this, type] { base::MutexGuard guard(&sweeping_mutex_); job_->Sweep(); job_finished_.NotifyAll(); }); job_->id_ = task->id(); V8::GetCurrentPlatform()->CallOnWorkerThread(std::move(task)); } 首先看 Prepare。 void ArrayBufferSweeper::Prepare(SweepingType type) { switch (type) { case SweepingType::kYoung: { job_ = std::make_unique type); young_ = ArrayBufferList(); } break; case SweepingType::kFull: { job_ = std::make_unique type); young_ = ArrayBufferList(); old_ = ArrayBufferList(); } break; } 这里根据 GC 类型创建一个 SweepingJob 任务和重置 young_ 队列(已经交给 SweepingJob 处理了),准备好之后,然后提交一个 task 给线程池。当线程池调度该任务执行时,就会执行 job_->Sweep()。 void ArrayBufferSweeper::SweepingJob::Sweep() { switch (type_) { case SweepingType::kYoung: SweepYoung(); break; case SweepingType::kFull: SweepFull(); break; } state_ = SweepingState::kDone; 根据 GC 类型进行处理,这里是新生代。 void ArrayBufferSweeper::SweepingJob::SweepYoung() { // 新生代当前待处理的队列 ArrayBufferExtension* current = young_.head_; ArrayBufferList new_young; ArrayBufferList new_old; // 遍历对象 while (current) { ArrayBufferExtension* next = current->next(); // 可以被 GC 了则直接删除 if (!current->IsYoungMarked()) { size_t bytes = current->accounting_length(); delete current; if (bytes) freed_bytes_.fetch_add(bytes, std::memory_order_relaxed); } else if (current->IsYoungPromoted()) { // 晋升到老生代,则把它重新放到老生代 current->YoungUnmark(); new_old.Append(current); } else { // 否则放回新生代 current->YoungUnmark(); new_young.Append(current); } current = next; } // GC 更新当前队列 old_ = new_old; young_ = new_young; } 遍历对象的过程中,V8 会把可以 GC 的对象直接删除,因为 ArrayBufferExtension 中是使用 std::shared_ptr 对 BackingStore 进行管理,所以 ArrayBufferExtension 被删除后,BackingStore 也会被删除,来看看 BackingStore 的析构函数。 BackingStore::~BackingStore() { // 是否需要在析构函数中销毁管理的内存,通常是需要 if (free_on_destruct_) { // 拿到内存分配器,然后释放之前申请的内存,通常是 free 函数 auto allocator = get_v8_api_array_buffer_allocator(); allocator->Free(buffer_start_, byte_length_); } // 重置字段 Clear(); 至此,就完成了 ArrayBuffer 的 GC 过程的分析。