Node中Buffer的初始化及回收

简介: node中的buffer相信大家都不会陌生,毕竟这个东西是node的核心之一,我们读写文件,网络请求都会用到它。不过,之前我虽然一直在用这个东西,却没关心过他的实现,只知道通过buffer分配的内存占用的不是v8的heap上的内存,存在于newSpace和oldSpace之外,所以可以用它来进行一些大段内存的操作,但是却从没关心过它是如何分配内存,又是什么时候被回收这些问题。在一次我的师兄奕钧的交

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初始化的过程

通过上面的分析我们再来看一下Buffer初始化和回收的整个过程,我们以下面最简单一个例子来分析:


(function(){
    let buf = new Buffer(8);
})()

从上面的分析我们知道,经历了以下的步骤:

  1. 在Buffer的构造函数中首先是调用new FastBuffer(8)
  2. 通过继承自v8::ArrayBuffer::Allocator的NodeArrayBufferAllocator对象中的Allocate方法中的UncheckedMalloc方法分配长度为8的堆内存
  3. JSArrayBuffer在V8内初始化时,会将上面分配的堆内存通过对象方法set_backing_store跟将这个v8 heap上的对象和堆上的内存关联起来
  4. 在函数结束后,这个buf会因为没有任何联系的对象而在下一次GC被回收掉。
  5. 回收的时候会调用ArrayBufferTracker::FreeDead,而在这个方法中跟分配时一样会调用NodeArrayBufferAllocator中的Free方法来释放堆内存。

总结

通过对源码的一番窥探,我们可以清楚的了解到了,为什么buffer的内存不存在v8的heap上,而且也知道了,对于buffer中内存的释放,其释放时机的判断跟普通的js对象是一样的。读完有没有感觉对buffer的使用心里有底了许多。这此探索对于我的收获很大,特别是在最新的Node-12.6.0代码中发现了NodeArrayBufferAllocator的派生类DebuggingArrayBufferAllocator,让我在对于做alinode关于堆外内存监控方面提供了不少的思路。

目录
相关文章
|
6月前
|
缓存 JavaScript 前端开发
【Node系列】Buffer详解
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。 但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。 Node.js中的Buffer是一个全局对象,属于固有(built-in)类型的全局变量,不需要使用require函数导入。它允许直接操作原始内存,主要用于处理二进制数据流。Buffer实例对象的结构和整数数组很像,但Buffer的大小是固定的且在V8堆外分配物理内存。
68 2
|
6月前
|
消息中间件 Web App开发 JavaScript
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
179 0
|
6月前
|
JavaScript 前端开发 API
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)(下)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
76 0
|
1月前
|
存储 JSON JavaScript
Node.js Buffer(缓冲区)
10月更文挑战第4天
26 4
|
6月前
|
消息中间件 Web App开发 JavaScript
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)(上)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
175 0
|
5月前
|
存储 JSON JavaScript
Node.js Buffer(缓冲区)
Node.js Buffer(缓冲区)
37 1
|
6月前
|
JavaScript 网络协议 数据处理
Node.js中的Buffer与Stream:深入解析与使用
【4月更文挑战第30天】本文深入解析了Node.js中的Buffer和Stream。Buffer是处理原始数据的全局对象,适用于TCP流和文件I/O,其大小在V8堆外分配。创建Buffer可通过`alloc`和`from`方法,它提供了读写、切片和转换等操作。Stream是处理流式数据的抽象接口,分为可读、可写、双工和转换四种类型,常用于处理大量数据而无需一次性加载到内存。通过监听事件和调用方法,如读取文件的可读流示例,可以实现高效的数据处理。理解和掌握Buffer及Stream能提升Node.js应用的性能。
|
6月前
|
JavaScript
Node.js之Buffer(缓冲器)
Node.js之Buffer(缓冲器)
|
存储 JavaScript 前端开发
Node.js 的 Buffer 是什么?探索其用途与功能
在 Node.js 中,Buffer 是一种用于处理二进制数据的机制。它允许你在不经过 JavaScript 垃圾回收机制的情况下直接操作原始内存,从而更高效地处理数据,特别是在处理网络流、文件系统操作和其他与 I/O 相关的任务时。Buffer 是一个全局对象,不需要额外的模块导入就可以使用。
|
6月前
|
存储 JavaScript 计算机视觉
什么是Node.js Buffer(缓冲区)?
什么是Node.js Buffer(缓冲区)?
80 1