前言: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 垃圾回收的实现相关推荐

  1. Unsafe堆外内存申请、回收

    在nio以前,是没有光明正大的做法的,唯一的办法是直接访问Unsafe类.如果你使用Eclipse,默认是不允许访问sun.misc下面的类的,你需要稍微修改一下,给Type Access Rules ...

  2. java nio 堆外内存_Java堆外内存之突破JVM枷锁

    对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM ...

  3. Java堆外内存:堆外内存回收方法

    一.JVM内存的分配及垃圾回收 对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下: 新生代:一般来说新创建的对象都分配在这里. 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面 ...

  4. java保证一段代码枷锁_Java堆外内存之突破JVM枷锁

    对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM ...

  5. java堆外内存6_Java 堆外内存的使用

    更多 Java 虚拟机方面的文章,请参见文集<Java 虚拟机> 为什么需要使用堆外内存 将长期存活的对象(如 Local Cache )移入堆外内存( off-heap,又名直接内存 d ...

  6. sun.misc.Cleaner实现堆外内存回收

    简介 项目中采用了java+c的混合开发,通过jni进行了底层结构体的内存分配,将指针返回给java层保存,随后则可以通过传递指针值来操作底层代码.在java中,仍然需要手动释放jni分配出来的内存的 ...

  7. 一文探讨堆外内存的监控与回收

    引子 记得那是一个风和日丽的周末,太阳红彤彤,花儿五颜六色,96 年的普哥微信找到我,描述了一个诡异的线上问题:线上程序使用了 NIO FileChannel 的 堆内内存作为缓冲区,读写文件,逻辑可 ...

  8. JVM堆外内存的回收机制分析

    本文来说下堆外内存的回收机制分析 文章目录 堆外内存 堆外内存的申请和释放 堆外内存的回收机制 本文小结 堆外内存 JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Ne ...

  9. JVM 上篇之内存与垃圾回收(个人笔记,勿看)

    内存与垃圾回收篇 字节码与类的加载篇 性能监控与调优篇 大厂面试篇 文章目录 JVM 跨语言的平台 虚拟机与Java虚拟机 虚拟机 Java 虚拟机 Java 代码的执行流程 JVM的架构模型 JVM ...

最新文章

  1. 迁移学习前沿研究亟需新鲜血液,深度学习理论不能掉链子
  2. python 细枝末节
  3. v-model数据绑定分析
  4. (10)CSS 常用样式--盒模型扩展应用
  5. IntelliJ IDEA这样配置,代码效率嗖嗖的
  6. hisicv200 exfat支持(转)
  7. [SecureCRT] 解决 securecrt failed to open the host key database file 的问题
  8. Shell编程-JAVA大数据-Week5-DAY3-linux
  9. mysql1026_mysql 启动错误1026
  10. 带K线的macd选股指标详解 优化MACD王牌指标 通达信macd选股指标源码
  11. 解决微信页面加载自动播放音乐
  12. 通信中台的概念界定与能力拆解
  13. zz麦考林(M18.com)多渠道狂奔
  14. 招行193亿港元收购永隆银行53.1%股份
  15. 公链分析报告(2)--EOS
  16. 已知信码序列为1011_专升本计算机网络:校验码
  17. activiti学习01
  18. 国内数据库顶会DTCC 阿里数据库技术干货全面解析
  19. PG:什么是grouping sets
  20. 类EMD的“信号分解方法”及MATLAB实现(第七篇)——EWT

热门文章

  1. 注塑机摆放间距多少合适_选用注塑机的基本原则
  2. 在 Able2Extract 中打开的 PDF 文档的某些部分显示为无法识别的字符?
  3. 3d打印 计算机芯片,提高计算机芯片数据路由能力!3D打印最小龙勃透镜诞生
  4. 【IPAM】Netbox docker模式版本升级
  5. 2018.11.22
  6. 网络流量监控与分析软件
  7. 湖北省星创天地备案和绩效评价申报,2022年条件流程及时间讲解
  8. 虚拟化概述及VMware VSphere介绍
  9. linux命令行was集群启停,通用服务启停shell脚本
  10. grep -v grep使用说明