Node.js的C++扩展教程(二)

梦想的初衷 提交于 2020-03-11 00:59:51

今天我们讲一下关于V8中的内存机制、隔离实例、上下文、脚本。

内存机制

1.基本概念

在Chrome V8中,内存机制是非常重要的,其中就包含它内在的各种概念。V8高效的一个重要原因就是它的内存机制。

Chrome V8中JavaScript的数据类型(Number、Object、Function等)都是由V8内部的内存机制进行管理的。也就是说如果你在自己的JavaScript代码中声明了一个变量/函数/对象,那么这个变量/函数/对象将被V8的内存机制进行管理。

Chrome V8所创建的C++类型能被你编写的C++代码(如C++扩展)所访问,并且其跟JavaScript中操作的是在内存中相同的存储单元。比如说你在JavaScript中声明了let a = 1;,那么在C++扩展中是可以被访问到的。

举个例子,在JavaScript中定义了一个字符串:

let a = "hello world"

Chrome V8会对应的创建一个v8::String类型的对象,里面有一堆Chrome V8内置的数据信息。

这些数据类型中都有对数据元信息的一个引用,用于内存管理。

2.堆内存类型

堆内存类型有以下几种:

  • 新生代内存区:基本的数据对象(小数据对象)都被分配到这里,其特点是小而频,也就是空间小,垃圾回收频繁。
  • 老生代指针区:这是一堆指向老生代内存区具体数据内容的指针。
  • 老生代数据区:存放数据对象,在新生代内存区中经过 “晋升” 的数据都会放到这里。
  • 大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收并不会移动大对象。
  • 代码区:代码对象,这里的代码是JavaScript经过JIT编译生成的对象。
  • Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map。

3.堆内存的垃圾回收机制

1.新生代的垃圾回收机制

新生代内存使用Scavenge算法进行回收,该算法的大致思想为,将内存一分为二,每部分的空间都被称为Semispace。在Semispace中,总有一个处于使用状态,称为From空间;另一个处于空闲状态,称为To空间。

在分配对象时,总使用From空间进行分配;在垃圾回收时,Chrome V8检查From空间中的存活对象,然后将这些对象复制到To空间中,剩下的对象就会被释放,完成复制后From空间和To空间的角色对调,原来的From空间变成了To空间,而原来的To空间就变成了From空间。

由此看出,新生代内存中,总有至少一半的内存是空闲不用的。

当一个新生代中的对象经过多次新生代的垃圾回收而继续坚挺在内存区域中,这个时候它就会被移动到老生代的数据区。这种操作叫作对象的晋升

2.老生代的垃圾回收机制

老生代所保存的对象大多数是生存周期很长的甚至是常驻内存的对象,而且老生代占用的内存较多。

老生代的垃圾回收机制是 Mark-Sweep(标记-清除)和 Mark-Compact(标记-整理)的结合体。其主要采用Mark-Sweep,如果老生代空间不足以分配从新生代晋升过来的对象时,才使用Mark-Compact。

(1)Mark-Sweep

  • 标记:在标记阶段需要遍历老生代堆中的所有对象,并标记哪些活着的对象,然后进入清除阶段。
  • 清除:在清除阶段,Chrome V8只清除没有被标记的对象。

(2)Mark-Compact

在标记-清除时,容易产生内存碎片的问题。所以Mark-Compact在标记清除的基础上进行修改,在 清除的时候让它们变得紧缩。就是在清除的时候,让活的对象尽量往前靠。

(3)惰性清理

在标记时,哪些对象是死的,哪些对象是活的,Chrome V8已经掌握了。但是清理释放是需要开销的,所以Chrome V8并不急着去清理,而是延迟执行。垃圾回收器可以根据自身需要来清理死掉的对象。

隔离实例

C++扩展中代码经常有 Isolate* isolate = args.GetIsolate(); 这句代码。我在《浅析Chrome浏览器的多进程架构》也写到一个Chrome运行程序有至少一个浏览器主进程、一个网络进程和若干个渲染进程。而这里一个引擎实例(运行在渲染进程中)对应的数据类型就是 Isolate(全称为 Isolate Instance)。

这是V8中所有要执行的地方都要出现的数据。它就是一个V8引擎的实例。每个实例内部拥有完全独立的各种状态,包括堆管理、垃圾回收等。

通过一个实例生成的任何对象都不能在另一个实例中使用。使用V8进行扩展开发的开发者是可以在他的程序里创建多个Isolate实例,并且并行地在多个线程中使用的——但是一个实例不能在多个线程中使用。

实例的创建代码如下:

//实例所必要的参数
v8::Isolate::CreateParams create_params;
//创建一个实例
v8::Isolate* isolate = v8::Isolate::New(create_params);

不过值得注意的是,在我们开发Node.js的C++扩展时,我们已经处于Chrome V8的环境中,这是就不需要再生成一个实例了,直接获取Node.js环境所使用的实例即可,如下代码:

void Method(const v8::FunctionCallbackInfo<Value>& args)
{
	Isolate* isolate = args.GetIsolate();
}

上下文

上下文对象是用来定义JavaScript执行环境的一个对象,其数据类型是Context,它在创建的时候需要指明属于哪个实例。

v8::Isolate* isolate = ...;
v8::Local<v8::Context> context = v8::Context::New(isolate);

这里可以理解为它是一个沙箱化的执行上下文环境,内部预置了一系列的对象和环境。

脚本

脚本(Script)就是一个包含一段已经编译好的JavaScript脚本的对象,数据类型就是Script。它在编译时就与一个处于活动状态的上下文进行绑定。

v8::Local<v8::Context> context = ...;
...
v8::Local<v8::String> source = 一段JavaScript代码;

//与上下文绑定并编译
v8::Local<v8::Value> result = v8::Script::Complie(context, source).ToLocalChecked();

//执行脚本
v8::Local<v8::Value> result2 = script->Run(context).ToLocalChecked();
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!