Node 中 Buffer 的初始化及回收

pagecao發表於2019-03-04

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.ccTYPED_ARRAY_NEW巨集實現中我們可以看到,類似Uint8ArrayTYPE_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的使用心裡有底了許多。

相關文章