node中的buffer相信大家都不會陌生,畢竟這個東西是node的核心之一,我們讀寫檔案,網路請求都會用到它。不過,之前我雖然一直在用這個東西,卻沒關心過他的實現,只知道通過buffer分配的記憶體佔用的不是v8的heap上的記憶體,存在於newSpace和oldSpace之外,所以可以用它來進行一些大段記憶體的操作,但是卻從沒關心過它是如何分配記憶體,又是什麼時候被回收這些問題。在一次有幸跟我神交已久的一位老哥的交流中,提起了這個問題才意識到自己這一塊上確實存在盲區,於是專程去node原始碼(v8.1.4)中去尋找了一番,也算是頗有所得,所以專門寫一篇文章記錄和分享一下。
buffer的初始化
首先,我們可以從lib/buffer.js
中,我們可以通過Buffer函式的程式碼往下追溯,發現Buffer的生成都是通過new FastBuffer
來生成的,而FastBuffer我們可以看到程式碼中是這樣實現的:
class FastBuffer extends Uint8Array
複製程式碼
這是繼承自一個Uint8Array
這個v8內部定義為TYPE_ARRAY
的型別,從v8在v8/src/api.cc
的TYPED_ARRAY_NEW
巨集實現中我們可以看到,類似Uint8Array
的TYPE_ARRAY
都是通過ArrayBuffer
來初始化的。
ArrayBuffer的實現
那麼既然Buffer用的是v8內部的物件ArrayBuffer
,那為什麼buffer分配的記憶體並不會統計到v8的heap中呢?這個問題需要我們通過觀察ArrayBuffer
是如何實現的,這裡我們可以通過src/node_buffer.cc
中的Buffer::New
的程式碼來解釋:
MaybeLocal<Object> New(Environment* env, size_t length) {
//判斷是否能生成
...
data = BufferMalloc(length);
Local<ArrayBuffer> ab =
ArrayBuffer::New(env->isolate(),data,length,ArrayBufferCreationMode::kInternalized);
Local<Uint8Array> ui = Uint8Array::New(ab, 0, length);
...
}
複製程式碼
從中我們可以看到,node原始碼中通過BufferMalloc
分配一段堆記憶體給初始化ArrayBuffer
使用,通過分析ArrayBuffer
的實現過程,我們可以在v8/src/objects.cc
中的JSArrayBuffer::Setup
方法中可以看到程式碼:
array_buffer->set_backing_store(data);
複製程式碼
通過這個方法將指向堆記憶體的指標跟ArrayBuffer
關聯起來,放入array_buffer
物件的backingstore
中,所以之前的問題就已經有了答案了,buffer中所使用的記憶體是通過malloc這樣的方式分配的堆記憶體,只是通過ArrayBuffer
物件關聯的js中使用。
Buffer的回收
說起Buffer的回收,我相信已經有聰明的讀者想到了,既然是通過js物件ArrayBuffer
關聯到js中使用,那肯定也能通過這個物件利用v8自身的gc來進行回收。沒錯,對於Buffer的回收也是依賴於ArrayBuffer
,在其中也是會根據ArrayBuffer
所在的oldSpace和newSpace的不同進行不同的回收方法,不過都是通過物件ArrayBufferTracker
來實現的。我們首先來看一下newSpace中的回收方案,在v8/src/heap/heap.cc
中的void Heap::Scavenge()
函式,這個是做新生代GC回收的函式,在這個函式中先通過正常的GC回收方案去判斷物件是否需要回收,而對於需要回收的ArrayBuffer
則是通過呼叫:
ArrayBufferTracker::FreeDeadInNewSpace(this);
複製程式碼
來完成的,而這個函式中會輪詢newSpace中所有的page,通過每個page中的LocalArrayBufferTracker
物件去輪詢其中儲存的每個頁中的ArrayBuffer
的資訊,判斷是否需要清理該物件的backingStore
,通過v8/src/heap/array-buffer-tracker.cc
中函式:
template <typename Callback>
void LocalArrayBufferTracker::Process(Callback callback) {
for (TrackingData::iterator it = array_buffers_.begin();
it != array_buffers_.end();) {
old_buffer = reinterpret_cast<JSArrayBuffer*>(*it);
...
if (result == kKeepEntry) {
...
} else if (result == kUpdateEntry) {
...
} else if (result == kRemoveEntry) {
//清理arrayBuffer中backingstore的記憶體
freed_memory += length;
old_buffer->FreeBackingStore();
it = array_buffers_.erase(it);
}
}
}
複製程式碼
而對於oldSpace中,則是通過v8/src/heap/mark-compact.cc
中的函式MarkCompactCollector::Sweeper::RawSweep
首先通過程式碼:
const MarkingState state = MarkingState::Internal(p);
複製程式碼
獲取page中所有物件標記情況的bitmap,接著通過該bitmap執行函式:
ArrayBufferTracker::FreeDead(p, state);
複製程式碼
通過這個函式來對page上需要釋放的ArrayBuffer
中的backingStore
進行釋放,利用也是page中的LocalArrayBufferTracker
物件,通過方法:
template <typename Callback>
void LocalArrayBufferTracker::Free(Callback should_free) {
...
for (TrackingData::iterator it = array_buffers_.begin();
it != array_buffers_.end();) {
JSArrayBuffer* buffer = reinterpret_cast<JSArrayBuffer*>(*it);
if (should_free(buffer)) {
freed_memory += length;
buffer->FreeBackingStore();
it = array_buffers_.erase(it);
} else {
...
}
}
...
}
複製程式碼
可以看到這部分的程式碼跟前面幾乎是一樣的。
總結
通過對原始碼的一番窺探,我們可以清楚的瞭解到了,為什麼buffer的記憶體不存在v8的heap上,而且也知道了,對於buffer中記憶體的釋放,其釋放時機的判斷跟普通的js物件是一樣的。讀完有沒有感覺對buffer的使用心裡有底了許多。