Node中Buffer的初始化及回收
发布于 5 年前 作者 peze 3468 次浏览 来自 分享

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的使用心里有底了许多。

19 回复

赞!虽然看得不是很明白,基础差了

请问buffer是如何malloc的?

new Buffer(8)

这个空间是直接malloc(8)出来的?能否讲讲。还有malloc具体是在哪执行的?

@zy445566 这个时候是直接通过 new FastBuffer(8);来分配的,在v8内部的话他是通过ArrayBufferAllocator对象的Allocate方法来分配的,在src/node.cc初始化isolate的那里创建了这个对象

ArrayBufferAllocator allocator;
params.array_buffer_allocator = &allocator;
Isolate* const isolate = Isolate::New(params);

然后 ArrayBufferAllocator对象里面有这个方法:

class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator {
public:
virtual void* Allocate(size_t length) {
	void* data = AllocateUninitialized(length);
 	return data == NULL ? data : memset(data, 0, length);
}
virtual void* AllocateUninitialized(size_t length) { return malloc(length); }

就在这里malloc的。

@peze 那Buffer.allocUnsafe(8) 也是这样做的吗?

@zy445566 大体是差不多的,只是这种的话,更类似于src/node_buffer里面的这种实现

Local<ArrayBuffer> ab =
ArrayBuffer::New(env->isolate(),
    data,
    length,
    ArrayBufferCreationMode::kInternalized);
Local<Uint8Array> ui = Uint8Array::New(ab, 0, length);	

可以写个钩子统计下 Buffer 分配的内存大小,针对堆外的泄漏很有参考价值

@peze 你的意思是Buffer.allocUnsafe(8)也会调用ArrayBufferAllocator的Allocate方法来生成?

@hyj1991 可以模仿SamplingHeapProfiler那种方式,在堆外内存上加一个观察对象,还能看到采样时间节点上是哪个函数分配了多大的内存这样。

@zy445566 Buffer.allocUnsafe在生成allocPool的时候直接使用createUnsafeArrayBuffer这个函数里面就是直接使用new ArrayBuffer(size),这个还是直接调用ArrayBuffer来分配的。

@peze 如果是这样的话,之前Allocate的方法已经填充了0

return data == NULL ? data : memset(data, 0, length);

也就是说node再次填充了一次随机值?

@zy445566 这个不是填充0吧,这个是把这段分配出来的堆内存里面的内容都清空,这是一个分配内存的初始化操作。

@peze 你可以试一下我下面的方法,我这边填充的是97(ascii的小a),输出也是aaaaaaaa,不知道是不是我的环境问题。 填充0是ascii的0.所以输出看不到,所以我改成97了

// test.cpp
#include<stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
    char* buffer = (char*)malloc(sizeof(char)*8);
    memset(buffer,97,sizeof(char)*8);
    printf("%s\n",buffer);
    return 0;
}

编译并运行:

gcc test.cpp  -o test && ./test 

@zy445566 额。。。这个没问题啊。。。所以我还是不太懂 到底有啥问题呢?

@peze 如果是Buffer.allocUnsafe(8),它的输出是内存中随机的值,但如果memset了,那么输出的值不应该是随机的

@zy445566 哦!这个我还真没注意到。。嗯。。。这个问题提得好 我再研究一下,谢谢

@peze 没事,共同进步🤝

@zy445566 我从 v8/src/builtins/builtins-arraybuffer.cc中找到了这个方法,应该就是我们平时调用的New ArrayBuffer(8)时候执行的construct方法

// This is a helper to construct an ArrayBuffer with uinitialized memory.
// This means the caller must ensure the buffer is totally initialized in
// all cases, or we will expose uinitialized memory to user code.
BUILTIN(ArrayBufferConstructor_DoNotInitialize) {
	HandleScope scope(isolate);
	Handle<JSFunction> target(isolate->native_context()->array_buffer_fun());
	Handle<Object> length = args.atOrUndefined(isolate, 1);
	return ConstructBuffer(isolate, target, target, length, false);
}

这个方法里面会直接调用ArrayBufferAllocator::AllocateUninitialized直接生成一段未初始化的内存。但是我还是有个疑惑因为这个方法上面有个BUILTIN(ArrayBufferConstructor_ConstructStub)这个会通过ArrayBufferAllocator::Allocate生成刚刚我们看到被初始化了的内存。什么时候初始化ArrayBuffer会调用这个构造函数呢?

在node.cc中 ArrayBufferAllocator::Allocate(size_t size)方法被重新实现过,并非之前说的v8::ArrayBuffer::Allocator::NewDefaultAllocator生成的ArrayBufferAllocator对象,这个方法如下:

void* ArrayBufferAllocator::Allocate(size_t size) {
	if (zero_fill_field_ || zero_fill_all_buffers)
		return node::UncheckedCalloc(size);
	else
		return node::UncheckedMalloc(size);
}

而不管在UncheckedMalloc与UncheckedCalloc中都是不会初始化这段分配的内存的,所以New ArrayBuffer(8)生成的内存中直接打印出来是随机的。上一个留言中说的ArrayBufferConstructor_DoNotInitialize在我的尝试中,发现New ArrayBuffer(8)并不会调用,而是调用正常的BUILTIN(ArrayBufferConstructor_ConstructStub)函数。因为发现了这个所以特别说明一下,免得误导了大家。

回到顶部