

V8 堆外内存 ArrayBuffer 垃圾回收的实现
source link: https://www.fly63.com/article/detial/11419
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

扫一扫分享
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 过程的分析。
来源: 编程杂技
链接: https://www.fly63.com/article/detial/11419
</div
Recommend
-
51
1. 介绍 浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程
-
67
前言 程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。所谓的内存泄漏简单来说是不再用到的内存,没有及时释放。为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制。 在C与C++等语言中,开发人员可以直接控制内存的申请和回收。
-
38
涤生的博客 转载请注明原创出处,谢谢 如果读完觉得有收获的话,欢迎点赞加关注 堆外内存简介 DirectByteBuffer 这个类是 JDK 提供使用堆外内存的一种途径,当然常见的业务开发一般不会接触到,即...
-
15
工欲善其事,必先利其器,本文之器非器具之器,乃容器也,言归正传,作为一个前端打工人,左手刚 const 定义常量,忠贞不二,转头就 new 几个对象,玩的火热,真是个优秀的 jser,风骚的操作背后,必有日夜不辍的 QWER,外加一个走 A,废话不多说,浏览器内核是...
-
13
由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以...
-
18
【摘要】今天带你走进JVM的世界。 学过Java程序员对JVM应该并不陌生,如果你没有听过,没关系今天我带你走进JVM的世界。程序员为什么要学习JVM呢,其实不懂JVM也可以照样写出优质的代码,但是不...
-
6
Java进阶 JVM 内存与垃圾回收篇(一) 1.1 什么是JVM? 定义...
-
5
@[toc] Python垃圾回收引用计数器为主,标记清除和分代回收为辅+缓存机制 1. 引用计数器1.1 环状双向链表 refchain在Python程序中创建的任何对象...
-
13
从 python 中的垃圾回收回顾下内存碎片化 发表于 2022-05-03...
-
3
【Java】 DirectByteBuffer堆外内存回收
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK