前言:对于基于 V8 的 JS 运行时来说,堆外内存的管理是非常重要的一部分,因为 gc 的原因,V8 自己管理堆内存大小是有限制的,我们不能什么数据都往 V8 的堆里存储,比如我们想一下读取一个 1G 的文件,如果存到 V8 的堆,一下子就满了,所以我们需要定义堆外内存并进行管理。本文介绍 No.js 里目前支持的简单堆内存管理机制和字符编码解码的实现。
1 字符串的使用
数据的读写,在底层都是一个个字节,那么我们在 JS 层定义的字符串,C 层是怎么获取的呢?比如我们在 JS 里调用自定义 log 函数打印日志。
代码语言:javascript复制log("hello");
我们来看看 JS 运行时中 log 函数的实现。
代码语言:javascript复制void No::Console::log(V8_ARGS) {
V8_ISOLATE
String::Utf8Value str(isolate, args[0]);
Log(*str);}
最终在 C 里可以通过 V8 提供的 String::Utf8Value 从 args 中获得 JS 层的字符串,然后调用系统函数把它打印到屏幕就行。但是这种形式使用的内容是 V8 的堆内存。那么如果我们需要操作一个非常大的字符串,那怎么办呢?这时候就需要使用 V8 提供的堆外内存机制 ArrayBuffer。
2 ArrayBuffer 的实现
我们看看这个类关于内存申请的一些实现细节。当我们在 JS 里执行以下代码时
代码语言:javascript复制new ArrayBuffer(1)
来看看 V8 的实现。
代码语言:javascript复制BUILTIN(ArrayBufferConstructor) {
// [[Construct]] args 为 JS 层的参数
Handle<JSReceiver> new_target = Handle<JSReceiver>::cast(args.new_target());
// JS 层定义的长度,即 ArrayBuffer 的第一个参数
Handle<Object> length = args.atOrUndefined(isolate, 1);
return ConstructBuffer(isolate,
target,
new_target,
number_length, // = length
number_max_length, // 空
InitializedFlag::kZeroInitialized);}
接着看 ConstructBuffer 。
代码语言:javascript复制Object ConstructBuffer(Isolate* isolate, Handle<JSFunction> target,
Handle<JSReceiver> new_target, Handle<Object> length,
Handle<Object> max_length, InitializedFlag initialized) {
// resizable = ResizableFlag::kNotResizable
ResizableFlag resizable = max_length.is_null() ? ResizableFlag::kNotResizable : ResizableFlag::kResizable;
// 申请一个 JSArrayBuffer 对象,不包括存储数据的内存
Handle<JSObject> result;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, result,
JSObject::New(target, new_target, Handle<AllocationSite>::null()));
auto array_buffer = Handle<JSArrayBuffer>::cast(result);
size_t byte_length;
size_t max_byte_length = 0;
// byte_length:需要申请的字节数,由 length Object 解析得到,并且校验申请的大小是否超过阈值
if (!TryNumberToSize(*length, &byte_length) ||
byte_length > JSArrayBuffer::kMaxByteLength) {
// ...
}
std::unique_ptr<BackingStore> backing_store;
// 申请存储数据的内存
backing_store = BackingStore::Allocate(isolate, byte_length, shared, initialized);
max_byte_length = byte_length;
// 保存ArrayBuffer 存储数据的内存
array_buffer->Attach(std::move(backing_store));
array_buffer->set_max_byte_length(max_byte_length);}
以上代码首先申请了一个 JSArrayBuffer 对象,但是申请的对象中不包括存储数据的内存,接着通过 BackingStore::Allocate 申请存储数据的内存,并且保存到 JSArrayBuffer 中。我们接着看 BackingStore::Allocate 的内存分配逻辑。
代码语言:javascript复制std::unique_ptr<BackingStore> BackingStore::Allocate(
Isolate* isolate, size_t byte_length, SharedFlag shared,
InitializedFlag initialized) {
void* buffer_start = nullptr;
// ArrayBuffer 的内存分配器,初始化 V8 的时候可以设置
auto allocator = isolate->array_buffer_allocator();
if (byte_length != 0) {
auto allocate_buffer = [allocator, initialized](size_t 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(...);
return std::unique_ptr<BackingStore>(result);}
我们看到最终通过 allocator->Allocate 分配内存,allocator 是在初始化 V8 的时候设置的,比如 No.js 设置的 ArrayBuffer::Allocator::NewDefaultAllocator()。
代码语言:javascript复制v8::ArrayBuffer::Allocator* v8::ArrayBuffer::Allocator::NewDefaultAllocator() {
return new ArrayBufferAllocator();}
我们看看 ArrayBufferAllocator。
代码语言:javascript复制class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator {
public:
void* Allocate(size_t length) override {
return page_allocator_->AllocatePages(nullptr, RoundUp(length, page_size_),
page_size_,
PageAllocator::kReadWrite);
}
private:
PageAllocator* page_allocator_ = internal::GetPlatformDataCagePageAllocator();
const size_t page_size_ = page_allocator_->AllocatePageSize();};
最终调用 page_allocator_ 去分配内存,从 page_allocator_ 的值 GetPlatformDataCagePageAllocator 我们可以看到这里是调用系统相关的函数去申请内存,比如 Linux 下的 mmap。至此我们看到了 ArrayBuffer 的内存由来,
3 ArrayBuffer 应用
有了 ArrayBuffer,我们就可以在 V8 堆之外申请内存了,我们看看 No.js 里怎么使用。
代码语言:javascript复制http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => {
// HTTP 响应的 body
const body = `...`;
// HTTP 响应报文
const response = `...`;
// 申请堆外内存
const responseBuffer = new ArrayBuffer(response.length);
// 把响应内容写入堆外内存
const bytes = new Uint8Array(responseBuffer);
for (let i = 0; i < response.length; i ) {
bytes[i] = response[i].charCodeAt(0);
}
// 发送给客户端
res.write(responseBuffer);});
接着我们看看 write 的实现。
代码语言:javascript复制// 拿到 JS 的 ArrayBuffer
Local<ArrayBuffer> arrayBuffer = args[1].As<ArrayBuffer>();
std::shared_ptr<BackingStore> backing = arrayBuffer->GetBackingStore();// 申请一个写请求struct io_request *io_req = (struct io_request *)malloc(sizeof(*io_req));memset(io_req, 0, sizeof(*io_req));// 拿到底层存储数据的内存,保存到 request 中等待发送
io_req->buf = backing->Data();
io_req->len = backing->ByteLength();
JS 层设置数据,然后在 C 层拿到存储数据的内存发送出去,这个看起来可以满足需求,但是似乎还不够,首先每次都要自己申请一个 ArrayBuffer 和 Uint8Array 比较麻烦,而且还需要自己设置 Uint8Array 的内容,最重要的是 Uint8Array 只能保存单字节的数据,如果我们要发送非单字节的字符就会出现问题了。比如 “