node - Buffer内存管理小结

1,579 阅读4分钟

前言

咱都知道,由于v8的限制,默认情况下64位的机器node最多使用1.4G左右的内存。如果需要使用更多的内存咋办呢? 一种办法是启动的时候通过--max-old-space-size手动把内存限制调高,但这么做的坏处是内存多了之后,每次垃圾回收的时间也会增长,这也是为啥v8默认只给1.4g的原因。 另外一种办法就是用buffer, 因为node的buffer所使用的内存放在堆外,也就没有内存的限制。

最近也遇到需要处理文件的场景,最后使用了buffer,但是用的时候还是有点慌,buffer的内存是怎么分配到堆外内存上的?既然是存放在堆外的内存上,那这部分内存是怎么被管理的?垃圾回收回收的时候会不会去扫这部分内存造成阻塞?

流程

之前文章分享过,node申请内存有两种方式,

  1. 当需要申请的内存比较大时,会直接通过native模块进行申请
  2. 从预先申请的FastBuffer池进行分配。

先来看第一种,直接分配:

void CreateFromString(const FunctionCallbackInfo<Value>& args) {
  CHECK(args[0]->IsString());
  CHECK(args[1]->IsString());

  enum encoding enc = ParseEncoding(args.GetIsolate(),
                                    args[1].As<String>(),
                                    UTF8);
  Local<Object> buf;

  if (New(args.GetIsolate(), args[0].As<String>(), enc).ToLocal(&buf))
    args.GetReturnValue().Set(buf);

可以看到这里调用了New(isolate, string, enc)这个函数,

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;

  if (length > 0) {
    data = UncheckedMalloc(length);  // 通过malloc分配内存
    ......
  }

  return scope.EscapeMaybe(New(isolate, data, actual));
}

可以看到,这里已经通过malloc分配了内存, 然后调用了另外一个new函数:

MaybeLocal<Object> New(Isolate* isolate, char* data, size_t length) {
  EscapableHandleScope handle_scope(isolate);
  Environment* env = Environment::GetCurrent(isolate);
  if (env == nullptr) {
    free(data);
    THROW_ERR_BUFFER_CONTEXT_NOT_AVAILABLE(isolate);
    return MaybeLocal<Object>();
  }
  Local<Object> obj;
  if (Buffer::New(env, data, length, true).ToLocal(&obj))
    return handle_scope.Escape(obj);
  return Local<Object>();
}

这里对env做了一下检测,然后调用了另外一个new函数,

MaybeLocal<Object> New(Environment* env,
                       char* data,
                       size_t length,
                       bool uses_malloc) {
  if (length > 0) {
    CHECK_NOT_NULL(data);
    CHECK(length <= kMaxLength);
  }

  if (uses_malloc) {
    if (!env->isolate_data()->uses_node_allocator()) {
      // We don't know for sure that the allocator is malloc()-based, so we need
      // to fall back to the FreeCallback variant.
      auto free_callback = [](char* data, void* hint) { free(data); };
      return New(env, data, length, free_callback, nullptr);
    } else {
      // This is malloc()-based, so we can acquire it into our own
      // ArrayBufferAllocator.
      CHECK_NOT_NULL(env->isolate_data()->node_allocator());
      env->isolate_data()->node_allocator()->RegisterPointer(data, length);  // 注册指针指向分配的内存
    }
  }

  Local<ArrayBuffer> ab =
      ArrayBuffer::New(env->isolate(),
                       data,
                       length,
                       ArrayBufferCreationMode::kInternalized);
  return Buffer::New(env, ab, 0, length).FromMaybe(Local<Object>());
}

可以看到这里分了两部,第一步是在node_allocator上注册了一个指针,指向之前分配的内存,第二步生调用ArrayBuffer::New函数成了一个新的arraybuffer,

Local<ArrayBuffer> v8::ArrayBuffer::New(
    Isolate* isolate,
    void* data,
    size_t byte_length,
    ArrayBufferCreationMode mode
) {
  // Embedders must guarantee that the external backing store is valid.
  CHECK(byte_length == 0 || data != nullptr);
  CHECK_LE(byte_length, i::JSArrayBuffer::kMaxByteLength);
  i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
  LOG_API(i_isolate, ArrayBuffer, New);
  ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
  i::Handle<i::JSArrayBuffer> obj =
      i_isolate->factory()->NewJSArrayBuffer(i::SharedFlag::kNotShared);
  i::JSArrayBuffer::Setup(
          obj,
          i_isolate,
          mode == ArrayBufferCreationMode::kExternalized,
          data,
          byte_length
      );
  return Utils::ToLocal(obj);
}

void JSArrayBuffer::Setup(
        Handle<JSArrayBuffer> array_buffer,
        Isolate* isolate,
        bool is_external,
        void* data,
        size_t byte_length,
        SharedFlag shared_flag,
        bool is_wasm_memory
){
  DCHECK_EQ(array_buffer->GetEmbedderFieldCount(),
            v8::ArrayBuffer::kEmbedderFieldCount);
  DCHECK_LE(byte_length, JSArrayBuffer::kMaxByteLength);
  for (int i = 0; i < v8::ArrayBuffer::kEmbedderFieldCount; i++) {
    array_buffer->SetEmbedderField(i, Smi::kZero);
  }
  array_buffer->set_byte_length(byte_length);
  array_buffer->set_bit_field(0);
  array_buffer->clear_padding();
  array_buffer->set_is_external(is_external);
  array_buffer->set_is_detachable(shared_flag == SharedFlag::kNotShared);
  array_buffer->set_is_shared(shared_flag == SharedFlag::kShared);
  array_buffer->set_is_wasm_memory(is_wasm_memory);
  
  // 重点
  array_buffer->set_backing_store(data);

  if (data && !is_external) {
    isolate->heap()->RegisterNewArrayBuffer(*array_buffer);
  }
}

可以看到,我们传入的data被传到了set_backing_store这个方法里:

void JSArrayBuffer::set_backing_store(void* value, WriteBarrierMode mode) {
  intptr_t ptr = reinterpret_cast<intptr_t>(value);
  WRITE_INTPTR_FIELD(*this, kBackingStoreOffset, ptr);
}

而这个方法看起来就是生成一个int指针,指向之前通过malloc申请的内存地址。 所以呢,看到这里前面的问题应该就很清楚了,通过这种方式申请的buffer,会在通过malloc申请一块内存,然后v8通过维护一个指向这块内存的指针来进行管理,当这个指针不再被引用的时候,这部分内存就会被回收掉。

那如果是通过FastBuffer池进行分配的呢? 其实和上面的步骤差不多,因为FastBuffer实际上就是一个Uint8Array, 而v8对typedArray的处理就是自动申请一个external array buffer:

  Local<Type##Array> Type##Array::New(Local<ArrayBuffer> array_buffer,    
                                      size_t byte_offset, size_t length) { 
    i::Isolate* isolate = Utils::OpenHandle(*array_buffer)->GetIsolate();  
    LOG_API(isolate, Type##Array, New);                                    
    ENTER_V8_NO_SCRIPT_NO_EXCEPTION(isolate);                              
    if (!Utils::ApiCheck(length <= kMaxLength,                             
                         "v8::" #Type                                      
                         "Array::New(Local<ArrayBuffer>, size_t, size_t)", 
                         "length exceeds max allowed value")) {            
      return Local<Type##Array>();                                         
    }                                                                      
    i::Handle<i::JSArrayBuffer> buffer = Utils::OpenHandle(*array_buffer); 
    i::Handle<i::JSTypedArray> obj = isolate->factory()->NewJSTypedArray( 
        i::kExternal##Type##Array, buffer, byte_offset, length);         
    return Utils::ToLocal##Type##Array(obj);   

参考

  1. node v12.0.0源码
  2. v8源码