V8 堆外内存 ArrayBuffer 垃圾回收的实现

开发 前端
本文介绍堆外内存的一种类型 ArrayBuffer 的 GC 实现。

V8 除了我们经常讲到的新生代和老生代的常规堆内存外,还有另一种堆内存,就是堆外内存。堆外内存本质上也是堆内存,只不过不是由 V8 进行分配,而是由 V8 的调用方分配,比如 Node.js,但是是由 V8 负责 GC 的。本文介绍堆外内存的一种类型 ArrayBuffer 的 GC 实现。

1.创建 ArrayBuffer

ArrayBuffer 的创建有很多种方式,比如在 JS 层创建 Uint8Array 或者 ArrayBuffer(对应实现 builtins-arraybuffer.cc),又比如自己在 C++ 层调用 V8 提供的 API 进行创建,它们最终对应的实现是一样的。为了简单起见,这里以通过 V8 API 创建的方式进行分析。对应头文件是 v8-array-buffer.h 的 ArrayBuffer。创建方式有很多种,这里以最简单的方式进行分析。

static Local<ArrayBuffer> New(Isolate* isolate, size_t byte_length);

通过调用 ArrayBuffer::New 就可以创建一个 ArrayBuffer 对象。来看看具体实现。

Local<ArrayBuffer> v8::ArrayBuffer::New(Isolate* isolate, size_t byte_length) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::MaybeHandle<i::JSArrayBuffer> result =
i_isolate->factory()->NewJSArrayBufferAndBackingStore(
byte_length, i::InitializedFlag::kZeroInitialized);

i::Handle<i::JSArrayBuffer> array_buffer;
if (!result.ToHandle(&array_buffer)) {
// ...
}

return Utils::ToLocal(array_buffer);
}

首先看 NewJSArrayBufferAndBackingStore。

MaybeHandle<JSArrayBuffer> Factory::NewJSArrayBufferAndBackingStore(
size_t byte_length, InitializedFlag initialized,
AllocationType allocation) {
std::unique_ptr<BackingStore> backing_store = nullptr;

if (byte_length > 0) {
// 分配一块内存
backing_store = BackingStore::Allocate(isolate(), byte_length,
SharedFlag::kNotShared, initialized);
}
// map 标记对象的类型
Handle<Map> map(isolate()->native_context()->array_buffer_fun().initial_map(),
isolate());
// 新建一个 JSArrayBuffer 对象,默认在新生代申请内存
auto array_buffer = Handle<JSArrayBuffer>::cast(NewJSObjectFromMap(map, allocation));
// 关联 JSArrayBuffer 和 内存
array_buffer->Setup(SharedFlag::kNotShared, ResizableFlag::kNotResizable,
std::move(backing_store));
return array_buffer;
}

NewJSArrayBufferAndBackingStore 的逻辑非常多,每一步都是需要了解的,我们逐句分析。

std::unique_ptr<BackingStore> BackingStore::Allocate(
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<BackingStore>(result);
}

首先获取 array_buffer_allocator 内存分配器,该分配器由 V8 的调用方提供,比如 Node.js 的 NodeArrayBufferAllocator。然后通过该分配器分配内存,通常是通过 calloc,malloc 等函数分配内存。不过这里不是直接分配,而是通过封装一个函数交给 AllocateExternalBackingStore 函数进行处理。

void* Heap::AllocateExternalBackingStore(
const std::function<void*(size_t)>& allocate, size_t byte_length) {
// 执行函数分配内存
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 中的具体实现。接着看。

auto array_buffer = Handle<JSArrayBuffer>::cast(NewJSObjectFromMap(map, allocation));

NewJSObjectFromMap 根据 map 在 allocation 指示的地方分配一个内存用来存储 JSArrayBuffer 对象。map 表示对象的类型,allocation 表示在哪个 space 分配这块内存,默认是新生代。来看下 NewJSObjectFromMap。

Handle<JSObject> Factory::NewJSObjectFromMap(
Handle<Map> map, AllocationType allocation,
Handle<AllocationSite> 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 对象。接着看最后一步。

array_buffer->Setup(SharedFlag::kNotShared, ResizableFlag::kNotResizable, std::move(backing_store));

Setup 是把申请的 BackingStore 对象和 JSArrayBuffer 对象关联起来,JSArrayBuffer 对象不涉及存储数据的内存,它只是保存了一些元信息,比如内存大小。具体存储数据的内存由 BackingStore 管理。看看 Setup 的实现。

void JSArrayBuffer::Setup(SharedFlag shared, ResizableFlag resizable,
std::shared_ptr<BackingStore> backing_store) {
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<BackingStore> backing_store) {

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<BackingStore>());
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 对象的创建。

2.ArrayBuffer GC 的实现

接着看 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<SweepingJob>(std::move(young_), ArrayBufferList(),
type);
young_ = ArrayBufferList();
} break;
case SweepingType::kFull: {
job_ = std::make_unique<SweepingJob>(std::move(young_), std::move(old_),
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 过程的分析。

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2023-02-28 07:56:07

V8内存管理

2022-04-29 08:00:51

V8垃圾回收

2021-10-05 20:12:57

No.jsV8 编码

2020-09-27 07:32:18

V8

2023-06-07 16:00:40

JavaScriptV8语言

2023-12-19 21:52:51

Go垃圾回收开发

2024-03-14 09:40:14

2022-05-06 23:03:48

V8CPUProfiler

2022-08-19 06:40:02

V8GC

2023-10-10 10:23:50

JavaScriptV8

2021-01-29 07:37:07

内存对象垃圾

2011-08-15 16:28:06

Cocoa内存管理

2017-04-25 14:39:55

JVM内存Java

2014-12-19 11:07:40

Java

2021-10-16 05:00:32

.js Buffer模块

2017-02-21 16:40:16

Android垃圾回收内存泄露

2023-05-31 09:00:00

2022-01-25 09:15:39

V8垃圾回收算法

2010-07-20 16:35:52

V8JavaScript浏览器

2022-07-03 20:31:59

JVMJava虚拟机
点赞
收藏

51CTO技术栈公众号