新聞中心
對于基于 V8 的 JS 運行時來說,堆外內(nèi)存的管理是非常重要的一部分,因為 gc 的原因,V8 自己管理堆內(nèi)存大小是有限制的,我們不能什么數(shù)據(jù)都往 V8 的堆里存儲,比如我們想一下讀取一個 1G 的文件,如果存到 V8 的堆,一下子就滿了,所以我們需要定義堆外內(nèi)存并進行管理。本文介紹 No.js 里目前支持的簡單堆內(nèi)存管理機制和字符編碼解碼的實現(xiàn)。

創(chuàng)新互聯(lián)建站是一家網(wǎng)站設(shè)計公司,集創(chuàng)意、互聯(lián)網(wǎng)應(yīng)用、軟件技術(shù)為一體的創(chuàng)意網(wǎng)站建設(shè)服務(wù)商,主營產(chǎn)品:響應(yīng)式網(wǎng)站開發(fā)、品牌網(wǎng)站建設(shè)、網(wǎng)絡(luò)營銷推廣。我們專注企業(yè)品牌在網(wǎng)站中的整體樹立,網(wǎng)絡(luò)互動的體驗,以及在手機等移動端的優(yōu)質(zhì)呈現(xiàn)。做網(wǎng)站、成都網(wǎng)站制作、移動互聯(lián)產(chǎn)品、網(wǎng)絡(luò)運營、VI設(shè)計、云產(chǎn)品.運維為核心業(yè)務(wù)。為用戶提供一站式解決方案,我們深知市場的競爭激烈,認真對待每位客戶,為客戶提供賞析悅目的作品,網(wǎng)站的價值服務(wù)。
1 字符串的使用
數(shù)據(jù)的讀寫,在底層都是一個個字節(jié),那么我們在 JS 層定義的字符串,C++ 層是怎么獲取的呢?比如我們在 JS 里調(diào)用自定義 log 函數(shù)打印日志。
- log("hello");
我們來看看 JS 運行時中 log 函數(shù)的實現(xiàn)。
- void No::Console::log(V8_ARGS) {
- V8_ISOLATE
- String::Utf8Value str(isolate, args[0]);
- Log(*str);
- }
最終在 C++ 里可以通過 V8 提供的 String::Utf8Value 從 args 中獲得 JS 層的字符串,然后調(diào)用系統(tǒng)函數(shù)把它打印到屏幕就行。但是這種形式使用的內(nèi)容是 V8 的堆內(nèi)存。那么如果我們需要操作一個非常大的字符串,那怎么辦呢?這時候就需要使用 V8 提供的堆外內(nèi)存機制 ArrayBuffer。
2 ArrayBuffer 的實現(xiàn)
我們看看這個類關(guān)于內(nèi)存申請的一些實現(xiàn)細節(jié)。當(dāng)我們在 JS 里執(zhí)行以下代碼時
- new ArrayBuffer(1)
來看看 V8 的實現(xiàn)。
- BUILTIN(ArrayBufferConstructor) {
- // [[Construct]] args 為 JS 層的參數(shù)
- Handle
new_target = Handle ::cast(args.new_target()); - // JS 層定義的長度,即 ArrayBuffer 的第一個參數(shù)
- Handle
- return ConstructBuffer(isolate,
- target,
- new_target,
- number_length, // = length
- number_max_length, // 空
- InitializedFlag::kZeroInitialized);
- }
接著看 ConstructBuffer 。
- Object ConstructBuffer(Isolate* isolate, Handle
target, - Handle
new_target, Handle - Handle
- // resizable = ResizableFlag::kNotResizable
- ResizableFlag resizable = max_length.is_null() ? ResizableFlag::kNotResizable : ResizableFlag::kResizable;
- // 申請一個 JSArrayBuffer 對象,不包括存儲數(shù)據(jù)的內(nèi)存
- Handle
result; - ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
- isolate, result,
- JSObject::New(target, new_target, Handle
::null())); - auto array_buffer = Handle
::cast(result); - size_t byte_length;
- size_t max_byte_length = 0;
- // byte_length:需要申請的字節(jié)數(shù),由 length Object 解析得到,并且校驗申請的大小是否超過閾值
- if (!TryNumberToSize(*length, &byte_length) ||
- byte_length > JSArrayBuffer::kMaxByteLength) {
- // ...
- }
- std::unique_ptr
backing_store; - // 申請存儲數(shù)據(jù)的內(nèi)存
- backing_store = BackingStore::Allocate(isolate, byte_length, shared, initialized);
- max_byte_length = byte_length;
- // 保存ArrayBuffer 存儲數(shù)據(jù)的內(nèi)存
- array_buffer->Attach(std::move(backing_store));
- array_buffer->set_max_byte_length(max_byte_length);
- }
以上代碼首先申請了一個 JSArrayBuffer 對象,但是申請的對象中不包括存儲數(shù)據(jù)的內(nèi)存,接著通過 BackingStore::Allocate 申請存儲數(shù)據(jù)的內(nèi)存,并且保存到 JSArrayBuffer 中。我們接著看 BackingStore::Allocate 的內(nèi)存分配邏輯。
- std::unique_ptr
BackingStore::Allocate( - Isolate* isolate, size_t byte_length, SharedFlag shared,
- InitializedFlag initialized) {
- void* buffer_start = nullptr;
- // ArrayBuffer 的內(nèi)存分配器,初始化 V8 的時候可以設(shè)置
- 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;
- };
- // 執(zhí)行 allocate_buffer 分配內(nèi)存
- buffer_start = isolate->heap()->AllocateExternalBackingStore(allocate_buffer, byte_length);
- }
- / 分配一個 BackingStore 對象管理上面申請的內(nèi)存
- auto result = new BackingStore(...);
- return std::unique_ptr
(result); - }
我們看到最終通過 allocator->Allocate 分配內(nèi)存,allocator 是在初始化 V8 的時候設(shè)置的,比如 No.js 設(shè)置的 ArrayBuffer::Allocator::NewDefaultAllocator()。
- v8::ArrayBuffer::Allocator* v8::ArrayBuffer::Allocator::NewDefaultAllocator() {
- return new ArrayBufferAllocator();
- }
我們看看 ArrayBufferAllocator。
- 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();
- };
最終調(diào)用 page_allocator_ 去分配內(nèi)存,從 page_allocator_ 的值 GetPlatformDataCagePageAllocator 我們可以看到這里是調(diào)用系統(tǒng)相關(guān)的函數(shù)去申請內(nèi)存,比如 Linux 下的 mmap。至此我們看到了 ArrayBuffer 的內(nèi)存由來,
3 ArrayBuffer 應(yīng)用
有了 ArrayBuffer,我們就可以在 V8 堆之外申請內(nèi)存了,我們看看 No.js 里怎么使用。
- http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => {
- // HTTP 響應(yīng)的 body
- const body = `...`;
- // HTTP 響應(yīng)報文
- const response = `...`;
- // 申請堆外內(nèi)存
- const responseBuffer = new ArrayBuffer(response.length);
- // 把響應(yīng)內(nèi)容寫入堆外內(nèi)存
- const bytes = new Uint8Array(responseBuffer);
- for (let i = 0; i < response.length; i++) {
- bytes[i] = response[i].charCodeAt(0);
- }
- // 發(fā)送給客戶端
- res.write(responseBuffer);
- });
接著我們看看 write 的實現(xiàn)。
- // 拿到 JS 的 ArrayBuffer
- Local
arrayBuffer = args[1].As (); - std::shared_ptr
backing = arrayBuffer->GetBackingStore();// 申請一個寫請求struct io_request *io_req = (struct io_request *)malloc(sizeof(*io_req));memset(io_req, 0, sizeof(*io_req));// 拿到底層存儲數(shù)據(jù)的內(nèi)存,保存到 request 中等待發(fā)送 - io_req->buf = backing->Data();
- io_req->len = backing->ByteLength();
JS 層設(shè)置數(shù)據(jù),然后在 C++ 層拿到存儲數(shù)據(jù)的內(nèi)存發(fā)送出去,這個看起來可以滿足需求,但是似乎還不夠,首先每次都要自己申請一個 ArrayBuffer 和 Uint8Array 比較麻煩,而且還需要自己設(shè)置 Uint8Array 的內(nèi)容,最重要的是 Uint8Array 只能保存單字節(jié)的數(shù)據(jù),如果我們要發(fā)送非單字節(jié)的字符就會出現(xiàn)問題了。比如 “??“ 在 JS 里長度是 2,底層占四個字節(jié)。
- '????'.length => 2
所以還需要封裝一個模塊處理這些問題。
4 Buffer
類似 Node.js,No.js 也提供 Buffer 模塊處理 V8 堆外內(nèi)存,但是 No.js 沒有 Node.js 實現(xiàn)的功能那么多。下面我們看看如何實現(xiàn)。
- class Buffer {
- bytes = null;
- memory = null;
- constructor({ length }) {
- this.memory = new ArrayBuffer(length);
- this.bytes = new Uint8Array(this.memory);
- this.byteLength = length;
- }
- static from(str) {
- const chars = toUTF8(str);
- const buffer = new Buffer({length: chars.length});
- for (let i = 0; i < buffer.byteLength; i++) {
- buffer.bytes[i] = chars[i];
- }
- return buffer;
- }
- static toString(bytes) {
- return fromUTF8(bytes);
- }
- }
使用的方式和 Node.js 一樣。
- Buffer.from("你好")
字符串通過 Buffer 類實現(xiàn),Buffer 封裝了 ArrayBuffer 和 Uint8Array,不過更重要的是實現(xiàn)了 UTF-8 編碼和解碼,這樣應(yīng)用層就可以傳任何字符串,Buffer 會轉(zhuǎn)成對應(yīng)的 UTF-8 編碼(一系列二進制數(shù)據(jù)),處理完后再通過底層傳輸就可以??匆幌?UTF-8 編碼解碼的實現(xiàn)。
- function toUTF8(str) {
- // 通過 ... 解決多字節(jié)字符問題
- const chars = [...str];
- const bytes = [];
- for (let i = 0; i < chars.length; i++) {
- const char = chars[i];
- const code = char.codePointAt(0);
- if (code > 0 && code < 0x7F) {
- bytes.push(code)
- } else if (code > 0x80 && code < 0x7FF) {
- bytes.push((code >> 6) & 0x1f | 0xC0);
- bytes.push(code & 0x3f | 0x80);
- } else if ((code > 0x800 && code < 0xFFFF) || (code > 0xE000 && code < 0xFFFF)) {
- bytes.push((code >> 12) & 0x0f | 0xE0);
- bytes.push((code >> 6) & 0x3f | 0x80);
- bytes.push(code & 0x3f | 0x80);
- } else if (code > 0x10000 && code < 0x10FFFF) {
- bytes.push((code >> 18) & 0x07 | 0xF0);
- bytes.push((code >> 12) & 0x3f | 0x80);
- bytes.push((code >> 6) & 0x3f | 0x80);
- bytes.push(code & 0x3f | 0x80);
- }
- }
- return bytes;
- }
toUTF8 把字符的 Unicode 碼變成 UTF-8 編碼,具體實現(xiàn)就是根據(jù) UTF-8 的規(guī)則,但是有一個地方需要注意的是,不能簡單遍歷 JS 字符串。比如 “??“ 在遍歷的時候情況如下
- '????'[0] => '\uD842''????'[1] => '\uDFB7'
所以需要處理一下使得每個字符變得一個獨立的元素,再獲得它的 unicode 碼進行處理。
- const chars = [...str];
接著看看 解碼。
- // 計算二進制數(shù)最左邊有多少個連續(xù)的 1
- function countByte(byte) {
- let bytelen = 0;
- while(byte & 0x80) {
- bytelen++;
- byte = (byte << 1) & 0xFF;
- }
- return bytelen || 1;}
- function fromUTF8(bytes) {
- let i = 0;
- const chars = [];
- while(i < bytes.length) {
- const byteLen = countByte(bytes[i]);
- switch(byteLen) {
- case 1:
- chars.push(String.fromCodePoint(bytes[i]));
- i += 1;
- break;
- case 2:
- chars.push(String.fromCodePoint( (bytes[i] & 0x1F) << 6 | (bytes[i + 1] & 0x3F) ));
- i += 2;
- break;
- case 3:
- chars.push(String.fromCodePoint( (bytes[i] & 0x0F) << 12 | (bytes[i + 1] & 0x3F) << 6| (bytes[i + 2] & 0x3F) ));
- i += 3;
- break;
- case 4:
- chars.push(String.fromCodePoint( (bytes[i] & 0x07) << 18 | (bytes[i + 1] & 0x3F) << 12 | (bytes[i + 2] & 0x3F) << 6 | (bytes[i + 3] & 0x3F) ));
- i += 4;
- break;
- default:
- throw new Error('invalid byte');
- }
- }
- return chars.join('');
- }
解碼的原理是首先計算單字節(jié)的最左邊有多少個 1,這個表示后續(xù)的多少個字節(jié)組成一個字符。計算完后就把一個或多個字節(jié)按照 UTF-8 規(guī)則拼出 unicode 碼,然后使用 fromCodePoint 轉(zhuǎn)成對應(yīng)字符。最后看看使用例子。
- http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => {
- const body = `
- 你好!
- `;
- res.setHeaders({
- "Content-Type": "text/html; charset=UTF-8"
- });
- res.end(body);
- });
5 總結(jié)
目前初步實現(xiàn)了堆外內(nèi)存管理和編碼解碼的功能,這樣應(yīng)用層就不需要面對麻煩的堆外內(nèi)存管理和數(shù)據(jù)設(shè)置問題。另外 V8 堆外內(nèi)存我們平時可能關(guān)注的不是很多,但是卻是一個重要的部分。
文章名稱:No.js中V8堆外內(nèi)存管理和字符編碼解碼的實現(xiàn)
分享路徑:http://fisionsoft.com.cn/article/cdhpjie.html


咨詢
建站咨詢
