深入理解 Node.js 的 Buffer
Buffer 模塊是 Node.js 非常重要的模塊,很多模塊都依賴它,本文介紹一下 Buffer 模塊底層的原理,包括 Buffer 的核心實現(xiàn)和 V8 堆外內(nèi)存等內(nèi)容。
1 Buffer 的實現(xiàn)
1.1 Buffer 的 JS 層實現(xiàn)
Buffer 模塊的實現(xiàn)雖然非常復(fù)雜,代碼也非常多,但是很多都是編碼解碼以及內(nèi)存分配管理的邏輯,我們從常用的使用方式 Buffer.from 來看看 Buffer 的核心實現(xiàn)。
- Buffer.from = function from(value, encodingOrOffset, length) {
- return fromString(value, encodingOrOffset);
- };
- function fromString(string, encoding) {
- return fromStringFast(string, ops);
- }
- function fromStringFast(string, ops) {
- const length = ops.byteLength(string);
- // 長度太長,從 C++ 層分配
- if (length >= (Buffer.poolSize >>> 1))
- return createFromString(string, ops.encodingVal);
- // 剩下的不夠了,擴(kuò)容
- if (length > (poolSize - poolOffset))
- createPool();
- // 從 allocPool (ArrayBuffer)中分配內(nèi)存
- let b = new FastBuffer(allocPool, poolOffset, length);
- const actual = ops.write(b, string, 0, length);
- poolOffset += actual;
- alignPool();
- return b;
- }
from 的邏輯如下:1. 如果長度大于 Node.js 設(shè)置的閾值,則調(diào)用 createFromString 通過 C++ 層直接分配內(nèi)存。2. 否則判斷之前剩下的內(nèi)存是否足夠,足夠則直接分配。Node.js 初始化時會首先分配一大塊內(nèi)存由 JS 管理,每次從這塊內(nèi)存了切分一部分給使用方,如果不夠則擴(kuò)容。我們看看 createPool。
- // 分配一個內(nèi)存池
- function createPool() {
- poolSize = Buffer.poolSize;
- // 拿到底層的 ArrayBuffer
- allocPool = createUnsafeBuffer(poolSize).buffer;
- poolOffset = 0;
- }
- function createUnsafeBuffer(size) {
- zeroFill[0] = 0;
- try {
- return new FastBuffer(size);
- } finally {
- zeroFill[0] = 1;
- }
- }
- class FastBuffer extends Uint8Array {}
我們看到最終調(diào)用 Uint8Array 實現(xiàn)了內(nèi)存分配。3. 通過 new FastBuffer(allocPool, poolOffset, length) 從內(nèi)存池中分配一塊內(nèi)存。如下圖所示。
1.2 Buffer 的 C++ 層實現(xiàn)
分析 C++ 層之前我們先看一下 V8 里下面幾個對象的關(guān)系圖。
接著來看看通過 createFromString 直接從 C++ 申請內(nèi)存的實現(xiàn)。
- void CreateFromString(const FunctionCallbackInfo<Value>& args) {
- enum encoding enc = static_cast<enum encoding>(args[1].As<Int32>()->Value());
- Local<Object> buf;
- if (New(args.GetIsolate(), args[0].As<String>(), enc).ToLocal(&buf))
- args.GetReturnValue().Set(buf);
- }
- MaybeLocal<Object> New(Isolate* isolate,
- Local<String> string,
- enum encoding enc) {
- EscapableHandleScope scope(isolate);
- size_t length;
- // 計算長度
- if (!StringBytes::Size(isolate, string, enc).To(&length))
- return Local<Object>();
- size_t actual = 0;
- char* data = nullptr;
- // 直接通過 realloc 在進(jìn)程堆上申請一塊內(nèi)存
- data = UncheckedMalloc(length);
- // 按照編碼轉(zhuǎn)換數(shù)據(jù)
- actual = StringBytes::Write(isolate, data, length, string, enc);
- return scope.EscapeMaybe(New(isolate, data, actual));
- }
- MaybeLocal<Object> New(Isolate* isolate, char* data, size_t length) {
- EscapableHandleScope handle_scope(isolate);
- Environment* env = Environment::GetCurrent(isolate);
- Local<Object> obj;
- if (Buffer::New(env, data, length).ToLocal(&obj))
- return handle_scope.Escape(obj);
- return Local<Object>();
- }
- MaybeLocal<Object> New(Environment* env,
- char* data,
- size_t length) {
- // JS 層變量釋放后使得這塊內(nèi)存沒人用了,GC 時在回調(diào)里釋放這塊內(nèi)存
- auto free_callback = [](char* data, void* hint) { free(data); };
- return New(env, data, length, free_callback, nullptr);
- }
- MaybeLocal<Object> New(Environment* env,
- char* data,
- size_t length,
- FreeCallback callback,
- void* hint) {
- EscapableHandleScope scope(env->isolate());
- // 創(chuàng)建一個 ArrayBuffer
- Local<ArrayBuffer> ab =
- CallbackInfo::CreateTrackedArrayBuffer(env, data, length, callback, hint);
- /*
- 創(chuàng)建一個 Uint8Array
- Buffer::New => Local<Uint8Array> ui = Uint8Array::New(ab, byte_offset, length)
- */
- MaybeLocal<Uint8Array> maybe_ui = Buffer::New(env, ab, 0, length);
- Local<Uint8Array> ui;
- if (!maybe_ui.ToLocal(&ui))
- return MaybeLocal<Object>();
- return scope.Escape(ui);
- }
通過一系列的調(diào)用,最后通過 CreateTrackedArrayBuffer 創(chuàng)建了一個 ArrayBuffer,再通過 ArrayBuffer 創(chuàng)建了一個 Uint8Array。接著看一下 CreateTrackedArrayBuffer 的實現(xiàn)。
- Local<ArrayBuffer> CallbackInfo::CreateTrackedArrayBuffer(
- Environment* env,
- char* data,
- size_t length,
- FreeCallback callback,
- void* hint) {
- // 管理回調(diào)
- CallbackInfo* self = new CallbackInfo(env, callback, data, hint);
- // 用自己申請的內(nèi)存創(chuàng)建一個 BackingStore,并設(shè)置 GC 回調(diào)
- std::unique_ptr<BackingStore> bs =
- ArrayBuffer::NewBackingStore(data, length, [](void*, size_t, void* arg) {
- static_cast<CallbackInfo*>(arg)->OnBackingStoreFree();
- }, self);
- // 通過 BackingStore 創(chuàng)建 ArrayBuffer
- Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(bs));
- return ab;
- }
看一下 NewBackingStore 的實現(xiàn)。
- std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
- void* data, size_t byte_length, v8::BackingStore::DeleterCallback deleter,
- void* deleter_data) {
- std::unique_ptr<i::BackingStoreBase> backing_store = i::BackingStore::WrapAllocation(data, byte_length, deleter, deleter_data,
- i::SharedFlag::kNotShared);
- return std::unique_ptr<v8::BackingStore>(
- static_cast<v8::BackingStore*>(backing_store.release()));
- }
- std::unique_ptr<BackingStore> BackingStore::WrapAllocation(
- void* allocation_base, size_t allocation_length,
- v8::BackingStore::DeleterCallback deleter, void* deleter_data,
- SharedFlag shared) {
- bool is_empty_deleter = (deleter == v8::BackingStore::EmptyDeleter);
- // 新建一個 BackingStore
- auto result = new BackingStore(allocation_base, // start
- allocation_length, // length
- allocation_length, // capacity
- shared, // shared
- false, // is_wasm_memory
- true, // free_on_destruct
- false, // has_guard_regions
- // 說明釋放內(nèi)存由調(diào)用方執(zhí)行
- true, // custom_deleter
- is_empty_deleter); // empty_deleter
- // 保存回調(diào)需要的信息
- result->type_specific_data_.deleter = {deleter, deleter_data};
- return std::unique_ptr<BackingStore>(result);
- }
NewBackingStore 最終是創(chuàng)建了一個 BackingStore 對象。我們再看一下 GC 時 BackingStore 的析構(gòu)函數(shù)里都做了什么。
- BackingStore::~BackingStore() {
- if (custom_deleter_) {
- type_specific_data_.deleter.callback(buffer_start_, byte_length_,
- type_specific_data_.deleter.data);
- Clear();
- return;
- }
- }
析構(gòu)的時候會執(zhí)行創(chuàng)建 BackingStore 時保存的回調(diào)。我們看一下管理回調(diào)的 CallbackInfo 的實現(xiàn)。
- CallbackInfo::CallbackInfo(Environment* env,
- FreeCallback callback,
- char* data,
- void* hint)
- : callback_(callback),
- data_(data),
- hint_(hint),
- env_(env) {
- env->AddCleanupHook(CleanupHook, this);
- env->isolate()->AdjustAmountOfExternalAllocatedMemory(sizeof(*this));
- }
CallbackInfo 的實現(xiàn)很簡單,主要的地方是 AdjustAmountOfExternalAllocatedMemory。該函數(shù)告訴 V8 堆外內(nèi)存增加了多少個字節(jié),V8 會根據(jù)內(nèi)存的數(shù)據(jù)做適當(dāng)?shù)? GC。CallbackInfo 主要是保存了回調(diào)和內(nèi)存地址。接著在 GC 的時候會回調(diào) CallbackInfo 的 OnBackingStoreFree。
- void CallbackInfo::OnBackingStoreFree() {
- std::unique_ptr<CallbackInfo> self { this };
- Mutex::ScopedLock lock(mutex_);
- // check 階段執(zhí)行 CallAndResetCallback
- env_->SetImmediateThreadsafe([self = std::move(self)](Environment* env) {
- self->CallAndResetCallback();
- });}void CallbackInfo::CallAndResetCallback() {
- FreeCallback callback;
- {
- Mutex::ScopedLock lock(mutex_);
- callback = callback_;
- callback_ = nullptr;
- }
- if (callback != nullptr) {
- // 堆外內(nèi)存減少了這么多個字節(jié)
- int64_t change_in_bytes = -static_cast<int64_t>(sizeof(*this));
- env_->isolate()->AdjustAmountOfExternalAllocatedMemory(change_in_bytes);
- // 執(zhí)行回調(diào),通常是釋放內(nèi)存
- callback(data_, hint_);
- }
- }
1.3 Buffer C++ 層的另一種實現(xiàn)
剛才介紹的 C++ 實現(xiàn)中內(nèi)存是由自己分配并釋放的,下面介紹另一種內(nèi)存的分配和釋放由 V8 管理的場景。以 Buffer 的提供的 EncodeUtf8String 函數(shù)為例,該函數(shù)實現(xiàn)字符串的編碼。
- static void EncodeUtf8String(const FunctionCallbackInfo<Value>& args) {
- Environment* env = Environment::GetCurrent(args);
- Isolate* isolate = env->isolate();
- // 被編碼的字符串
- Local<String> str = args[0].As<String>();
- size_t length = str->Utf8Length(isolate);
- // 分配內(nèi)存
- AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, length);
- // 編碼
- str->WriteUtf8(isolate,
- buf.data(),
- -1, // We are certain that `data` is sufficiently large
- nullptr,
- String::NO_NULL_TERMINATION | String::REPLACE_INVALID_UTF8);
- // 基于上面申請的 buf 內(nèi)存新建一個 Uint8Array
- auto array = Uint8Array::New(buf.ToArrayBuffer(), 0, length);
- args.GetReturnValue().Set(array);
- }
我們重點分析 AllocatedBuffer::AllocateManaged。
- AllocatedBuffer AllocatedBuffer::AllocateManaged(
- Environment* env,
- size_t size) {
- NoArrayBufferZeroFillScope no_zero_fill_scope(env->isolate_data());
- std::unique_ptr<v8::BackingStore> bs = v8::ArrayBuffer::NewBackingStore(env->isolate(), size);
- return AllocatedBuffer(env, std::move(bs));
- }
AllocateManaged 調(diào)用 NewBackingStore 申請了內(nèi)存。
- std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
- Isolate* isolate, size_t byte_length) {
- i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
- std::unique_ptr<i::BackingStoreBase> backing_store =
- i::BackingStore::Allocate(i_isolate, byte_length,
- i::SharedFlag::kNotShared,
- i::InitializedFlag::kZeroInitialized);
- return std::unique_ptr<v8::BackingStore>(
- static_cast<v8::BackingStore*>(backing_store.release()));
- }
繼續(xù)看 BackingStore::Allocate。
- std::unique_ptr<BackingStore> BackingStore::Allocate(
- Isolate* isolate, size_t byte_length, SharedFlag shared,
- InitializedFlag initialized) {
- void* buffer_start = nullptr;
- // ArrayBuffer 內(nèi)存分配器,可以自定義,V8 默認(rèn)提供的是使用平臺相關(guān)的堆內(nèi)存分析函數(shù),比如 malloc
- auto allocator = isolate->array_buffer_allocator();
- if (byte_length != 0) {
- auto allocate_buffer = [allocator, initialized](size_t byte_length) {
- // 分配內(nèi)存
- 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(buffer_start, // start
- byte_length, // length
- byte_length, // capacity
- shared, // shared
- false, // is_wasm_memory
- true, // free_on_destruct
- false, // has_guard_regions
- false, // custom_deleter
- false); // empty_deleter
- return std::unique_ptr<BackingStore>(result);
- }
BackingStore::Allocate 分配一塊內(nèi)存并新建 BackingStore 對象管理這塊內(nèi)存,內(nèi)存分配器是在初始化 V8 的時候設(shè)置的。這里我們再看一下 AllocateExternalBackingStore 函數(shù)的邏輯。
- void* Heap::AllocateExternalBackingStore(
- const std::function<void*(size_t)>& allocate, size_t byte_length) {
- // 可能需要觸發(fā) GC
- if (!always_allocate()) {
- size_t new_space_backing_store_bytes =
- new_space()->ExternalBackingStoreBytes();
- if (new_space_backing_store_bytes >= 2 * kMaxSemiSpaceSize &&
- new_space_backing_store_bytes >= byte_length) {
- CollectGarbage(NEW_SPACE,
- GarbageCollectionReason::kExternalMemoryPressure);
- }
- }
- // 分配內(nèi)存
- void* result = allocate(byte_length);
- // 成功則返回
- if (result) return result;
- // 失敗則進(jìn)行 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);
- }
我們看到通過 BackingStore 申請內(nèi)存失敗時會觸發(fā) GC 來騰出更多的可用內(nèi)存。分配完內(nèi)存后,最終以 BackingStore 對象為參數(shù),返回一個 AllocatedBuffer 對象。
- AllocatedBuffer::AllocatedBuffer(
- Environment* env, std::unique_ptr<v8::BackingStore> bs)
- : env_(env), backing_store_(std::move(bs)) {}
接著把 AllocatedBuffer 對象轉(zhuǎn)成 ArrayBuffer 對象。
- v8::Local<v8::ArrayBuffer> AllocatedBuffer::ToArrayBuffer() {
- return v8::ArrayBuffer::New(env_->isolate(), std::move(backing_store_));
- }
最后把 ArrayBuffer 對象傳入 Uint8Array 返回一個 Uint8Array 對象返回給調(diào)用方。
2 Uint8Array 的使用和實現(xiàn)
從前面的實現(xiàn)中可以看到 C++ 層的實現(xiàn)中,內(nèi)存都是從進(jìn)程的堆中分配的,那么 JS 層通過 Uint8Array 申請的內(nèi)存是否也是在進(jìn)程堆中申請的呢?下面我們看看 V8 中 Uint8Array 的實現(xiàn)。Uint8Array 有多種創(chuàng)建方式,我們只看 new Uint8Array(length) 的實現(xiàn)。
- transitioning macro ConstructByLength(implicit context: Context)(
- map: Map, lengthObj: JSAny,
- elementsInfo: typed_array::TypedArrayElementsInfo): JSTypedArray {
- try {
- // 申請的內(nèi)存大小
- const length: uintptr = ToIndex(lengthObj);
- // 拿到創(chuàng)建 ArrayBuffer 的函數(shù)
- const defaultConstructor: Constructor = GetArrayBufferFunction();
- const initialize: constexpr bool = true;
- return TypedArrayInitialize(
- initialize, map, length, elementsInfo, defaultConstructor)
- otherwise RangeError;
- }
- }
- transitioning macro TypedArrayInitialize(implicit context: Context)(
- initialize: constexpr bool, map: Map, length: uintptr,
- elementsInfo: typed_array::TypedArrayElementsInfo,
- bufferConstructor: JSReceiver): JSTypedArray labels IfRangeError {
- const byteLength = elementsInfo.CalculateByteLength(length);
- const byteLengthNum = Convert<Number>(byteLength);
- const defaultConstructor = GetArrayBufferFunction();
- const byteOffset: uintptr = 0;
- try {
- // 創(chuàng)建 JSArrayBuffer
- const buffer = AllocateEmptyOnHeapBuffer(byteLength);
- const isOnHeap: constexpr bool = true;
- // 通過 buffer 創(chuàng)建 TypedArray
- const typedArray = AllocateTypedArray(
- isOnHeap, map, buffer, byteOffset, byteLength, length);
- // 內(nèi)存置 0
- if constexpr (initialize) {
- const backingStore = typedArray.data_ptr;
- typed_array::CallCMemset(backingStore, 0, byteLength);
- }
- return typedArray;
- }
- }
主要邏輯分為兩步,首先通過 AllocateEmptyOnHeapBuffer 申請一個 JSArrayBuffer,然后以 JSArrayBuffer 創(chuàng)建一個 TypedArray。我們先看一下 AllocateEmptyOnHeapBuffer。
- TNode<JSArrayBuffer> TypedArrayBuiltinsAssembler::AllocateEmptyOnHeapBuffer(
- TNode<Context> context, TNode<UintPtrT> byte_length) {
- TNode<NativeContext> native_context = LoadNativeContext(context);
- TNode<Map> map = CAST(LoadContextElement(native_context, Context::ARRAY_BUFFER_MAP_INDEX));
- TNode<FixedArray> empty_fixed_array = EmptyFixedArrayConstant();
- // 申請一個 JSArrayBuffer 對象所需要的內(nèi)存
- TNode<JSArrayBuffer> buffer = UncheckedCast<JSArrayBuffer>(Allocate(JSArrayBuffer::kSizeWithEmbedderFields));
- // 初始化對象的屬性
- StoreMapNoWriteBarrier(buffer, map);
- StoreObjectFieldNoWriteBarrier(buffer, JSArray::kPropertiesOrHashOffset, empty_fixed_array);
- StoreObjectFieldNoWriteBarrier(buffer, JSArray::kElementsOffset, empty_fixed_array);
- int32_t bitfield_value = (1 << JSArrayBuffer::IsExternalBit::kShift) |
- (1 << JSArrayBuffer::IsDetachableBit::kShift);
- StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kBitFieldOffset, Int32Constant(bitfield_value));
- StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kByteLengthOffset, byte_length);
- // 設(shè)置 buffer 為 nullptr
- StoreJSArrayBufferBackingStore(buffer, EncodeExternalPointer(ReinterpretCast<RawPtrT>(IntPtrConstant(0))));
- StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kExtensionOffset, IntPtrConstant(0));
- for (int offset = JSArrayBuffer::kHeaderSize; offset < JSArrayBuffer::kSizeWithEmbedderFields; offset += kTaggedSize) {
- StoreObjectFieldNoWriteBarrier(buffer, offset, SmiConstant(0));
- }
- return buffer;
- }
AllocateEmptyOnHeapBuffer 申請了一個空的 JSArrayBuffer 對象,空的意思是說沒有存儲數(shù)據(jù)的內(nèi)存。接著看基于 JSArrayBuffer 對象 通過 AllocateTypedArray 創(chuàng)建一個 TypedArray。
- transitioning macro AllocateTypedArray(implicit context: Context)(
- isOnHeap: constexpr bool, map: Map, buffer: JSArrayBuffer,
- byteOffset: uintptr, byteLength: uintptr, length: uintptr): JSTypedArray {
- // 從 V8 堆中申請存儲數(shù)據(jù)的內(nèi)存
- let elements: ByteArray = AllocateByteArray(byteLength);
- // 申請一個 JSTypedArray 對象
- const typedArray = UnsafeCast<JSTypedArray>(AllocateFastOrSlowJSObjectFromMap(map));
- // 初始化屬性
- typedArray.elements = elements;
- typedArray.buffer = buffer;
- typedArray.byte_offset = byteOffset;
- typedArray.byte_length = byteLength;
- typedArray.length = length;
- typed_array::SetJSTypedArrayOnHeapDataPtr(typedArray, elements, byteOffset);
- SetupTypedArrayEmbedderFields(typedArray);
- return typedArray;
我們發(fā)現(xiàn) Uint8Array 申請的內(nèi)存是基于 V8 堆的,而不是 V8 的堆外內(nèi)存,這難道和 C++ 層的實現(xiàn)不一樣?Uint8Array 的內(nèi)存的確是基于 V8 堆的,比如我像下面這樣使用的時候。
- const arr = new Uint8Array(1);
- arr[0] = 65;
但是如果我們使用 arr.buffer 的時候,情況就不一樣了。我們看看具體的實現(xiàn)。
- BUILTIN(TypedArrayPrototypeBuffer) {
- HandleScope scope(isolate);
- CHECK_RECEIVER(JSTypedArray, typed_array,
- "get %TypedArray%.prototype.buffer");
- return *typed_array->GetBuffer();
- }
接著看 GetBuffer 的實現(xiàn)。
- Handle<JSArrayBuffer> JSTypedArray::GetBuffer() {
- Isolate* isolate = GetIsolate();
- Handle<JSTypedArray> self(*this, isolate);
- // 拿到 TypeArray 對應(yīng)的 JSArrayBuffer 對象
- Handle<JSArrayBuffer> array_buffer(JSArrayBuffer::cast(self->buffer()), isolate);
- // 分配過了直接返回
- if (!is_on_heap()) {
- return array_buffer;
- }
- size_t byte_length = self->byte_length();
- // 申請 byte_length 字節(jié)內(nèi)存存儲數(shù)據(jù)
- auto backing_store = BackingStore::Allocate(isolate, byte_length, SharedFlag::kNotShared, InitializedFlag::kUninitialized);
- // 關(guān)聯(lián) backing_store 到 array_buffer
- array_buffer->Setup(SharedFlag::kNotShared, std::move(backing_store));
- return array_buffer;
- }
我們看到當(dāng)使用 buffer 的時候,V8 會在 V8 堆外申請內(nèi)存來替代初始化 Uint8Array 時在 V8 堆內(nèi)分配的內(nèi)存,并且把原來的數(shù)據(jù)復(fù)制過來??匆幌孪旅娴睦?。
- console.log(process.memoryUsage().arrayBuffers)
- let a = new Uint8Array(10);
- a[0] = 65;
- console.log(process.memoryUsage().arrayBuffers)
我們會發(fā)現(xiàn) arrayBuffers 的值是一樣的,說明 Uint8Array 初始化時沒有通過 arrayBuffers 申請堆外內(nèi)存。接著再看下一個例子。
- console.log(process.memoryUsage().arrayBuffers)
- let a = new Uint8Array(1);
- a[0] = 65;
- a.buffer
- console.log(process.memoryUsage().arrayBuffers)
- console.log(new Uint8Array(a.buffer))
我們看到輸出的內(nèi)存增加了一個字節(jié),輸出的 a.buffer 是 [ 65 ](申請內(nèi)存超 64 時會從堆外申請)。
3 堆外內(nèi)存的管理
從之前的分析中我們看到,Node.js Buffer 是基于堆外內(nèi)存實現(xiàn)的(自己申請進(jìn)程堆內(nèi)存或者使用 V8 默認(rèn)的內(nèi)存分配器),我們知道,平時使用的變量都是由 V8 負(fù)責(zé)管理內(nèi)存的,那么 Buffer 所代表的堆外內(nèi)存是怎么管理的呢?Buffer 的內(nèi)存釋放也是由 V8 跟蹤的,不過釋放的邏輯和堆內(nèi)內(nèi)存不太一樣。我們通過一些例子來分析一下。
- function forceGC() {
- new ArrayBuffer(1024 * 1024 * 1024);
- }
- setTimeout(() => {
- /*
- 從 C++ 層調(diào)用 V8 對象創(chuàng)建內(nèi)存
- let a = process.binding('buffer').createFromString("你好", 1);
- */
- /*
- 直接使用 V8 內(nèi)置對象
- let a = new ArrayBuffer(10);
- */
- // 從 C++ 層自己管理內(nèi)存
- let a = process.binding('buffer').encodeUtf8String("你好");
- // 置空等待 GC
- a = null;
- // 分配一塊大內(nèi)存觸發(fā) GC
- process.nextTick(forceGC);
- }, 1000);
- const net = require('net');
- net.createServer((socket) => {}).listen()
在 V8 的代碼打斷點,然后調(diào)試以上代碼。
我們看到在超時回調(diào)里 V8 分配了一個 ArrayBufferExtension 對象并記錄到 ArrayBufferSweeper 中。接著看一下觸發(fā) GC 時的邏輯。
V8 在 GC 中會調(diào)用
heap_->array_buffer_sweeper()->RequestSweepYoung() 回收堆外內(nèi)存,另外 Node.js 本身似乎也使用線程去回收 堆外內(nèi)存。我們再看一下自己管理內(nèi)存的情況下回調(diào)的觸發(fā)。
如果這樣寫是不會觸發(fā) BackingStore::~BackingStore 執(zhí)行的,再次驗證了 Uint8Array 初始化時沒有使用 BackingStore。
- setTimeout(() => {
- let a = new Uint8Array(1);
- // a.buffer;
- a = null;
- process.nextTick(forceGC);
- });
但是如果把注釋打開就可以。
4 總結(jié)
Buffer 平時用起來可能比較簡單,但是如果深入研究它的實現(xiàn)就會發(fā)現(xiàn)涉及的內(nèi)容不僅多,而且還復(fù)雜,不過深入理解了它的底層實現(xiàn)后,會有種豁然開朗的感覺,另外 Buffer 的內(nèi)存是堆外內(nèi)存,如果我們發(fā)現(xiàn)進(jìn)程的內(nèi)存不斷增長但是 V8 堆快照大小變化不大,那可能是 Buffer 變量沒有釋放,理解實現(xiàn)能幫助我們更好地思考問題和解決問題。
作者