V8 堆外内存 ArrayBuffer 垃圾回收的实现
前言: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, // startbyte_length, // lengthbyte_length, // max lengthbyte_length, // capacityshared, // sharedResizableFlag::kNotResizable, // resizablefalse, // is_wasm_memorytrue, // free_on_destructfalse, // has_guard_regionsfalse, // custom_deleterfalse); // 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 过程的分析。
V8 堆外内存 ArrayBuffer 垃圾回收的实现相关推荐
- Unsafe堆外内存申请、回收
在nio以前,是没有光明正大的做法的,唯一的办法是直接访问Unsafe类.如果你使用Eclipse,默认是不允许访问sun.misc下面的类的,你需要稍微修改一下,给Type Access Rules ...
- java nio 堆外内存_Java堆外内存之突破JVM枷锁
对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM ...
- Java堆外内存:堆外内存回收方法
一.JVM内存的分配及垃圾回收 对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下: 新生代:一般来说新创建的对象都分配在这里. 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面 ...
- java保证一段代码枷锁_Java堆外内存之突破JVM枷锁
对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM ...
- java堆外内存6_Java 堆外内存的使用
更多 Java 虚拟机方面的文章,请参见文集<Java 虚拟机> 为什么需要使用堆外内存 将长期存活的对象(如 Local Cache )移入堆外内存( off-heap,又名直接内存 d ...
- sun.misc.Cleaner实现堆外内存回收
简介 项目中采用了java+c的混合开发,通过jni进行了底层结构体的内存分配,将指针返回给java层保存,随后则可以通过传递指针值来操作底层代码.在java中,仍然需要手动释放jni分配出来的内存的 ...
- 一文探讨堆外内存的监控与回收
引子 记得那是一个风和日丽的周末,太阳红彤彤,花儿五颜六色,96 年的普哥微信找到我,描述了一个诡异的线上问题:线上程序使用了 NIO FileChannel 的 堆内内存作为缓冲区,读写文件,逻辑可 ...
- JVM堆外内存的回收机制分析
本文来说下堆外内存的回收机制分析 文章目录 堆外内存 堆外内存的申请和释放 堆外内存的回收机制 本文小结 堆外内存 JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Ne ...
- JVM 上篇之内存与垃圾回收(个人笔记,勿看)
内存与垃圾回收篇 字节码与类的加载篇 性能监控与调优篇 大厂面试篇 文章目录 JVM 跨语言的平台 虚拟机与Java虚拟机 虚拟机 Java 虚拟机 Java 代码的执行流程 JVM的架构模型 JVM ...
最新文章
- 迁移学习前沿研究亟需新鲜血液,深度学习理论不能掉链子
- python 细枝末节
- v-model数据绑定分析
- (10)CSS 常用样式--盒模型扩展应用
- IntelliJ IDEA这样配置,代码效率嗖嗖的
- hisicv200 exfat支持(转)
- [SecureCRT] 解决 securecrt failed to open the host key database file 的问题
- Shell编程-JAVA大数据-Week5-DAY3-linux
- mysql1026_mysql 启动错误1026
- 带K线的macd选股指标详解 优化MACD王牌指标 通达信macd选股指标源码
- 解决微信页面加载自动播放音乐
- 通信中台的概念界定与能力拆解
- zz麦考林(M18.com)多渠道狂奔
- 招行193亿港元收购永隆银行53.1%股份
- 公链分析报告(2)--EOS
- 已知信码序列为1011_专升本计算机网络:校验码
- activiti学习01
- 国内数据库顶会DTCC 阿里数据库技术干货全面解析
- PG:什么是grouping sets
- 类EMD的“信号分解方法”及MATLAB实现(第七篇)——EWT