0%

《Node.js,来一打 Cpp 拓展》学习笔记- Chrome V8 基础-上

Chrome V8 基础-上

Node.js 与 Chrome V8

Node.js 的 JavaScript 运行时引擎是 Chrome V8,那么它们是以何种形式链接起来的呢?

  • 2006 年,V8 开始投入研发
  • 2008年9月,V8 发布了第一个版本,并且 Chrome 浏览器也几乎同期发布,至此 js 还是运行在浏览器中的一门脚本语言
  • 2009年, ry 开发了 Node.js

之后 js 就被带入后端领域,V8 也不仅仅只是 Chrome 的一个支持引擎。

与 Chrome 差不多,node 为 v8 提供了一个宿主,只不过前者的宿主中包含的是类似于 HTML DOM、window 对象等内容;而后者则提供了一整个沙箱 vm机制,以及文件系统、网络操作等内容。

也就是说,Node.js 实际上就是 Chrome V8 引擎的一个宿主。如果你有兴趣,也完全可以用 Chrome V8 创造一个别的 .js ,比如 Mode.jsLode.js等。

因此,我们并不需要对 Chrome V8 有一个特别深入的了解,也不需要知道它的算法、原理等。我们只用关心它暴露出来的一些 api,以及使用这些 api 所必要的储备知识。

有了这些 api,我们就能让自己的 C++ 扩展与 Node.js 进行互通了 —— 因为 Node.js 底层很大程度上也是直接使用了 Chrome V8 所暴露出来的 api。

基本概念

内存机制

Chrome V8 中,内存机制是很重要的,其中就包括了它在内的各种概念。v8 高效的一个重要原因就是因为它的内存机制。

Chrome V8 中 js 的数据类型都是由 v8 内部的内存机制进行管理的。

Node.js 实际上就是一个使用 C++ 完成的程序,其能执行 js 代码,它的底层主要由两部分第三方库组成——Chrome V8 和 libuv:

  • Chrome V8 是 js 运行时,用于解释执行 js
  • libuv 实现了 node 中大家老生常谈的 “事件循环”。

其中 v8 是一个由 C++ 完成的库,用于执行 js。也就是说如果你在自己的 js 代码中声明了一个变量,那么这个变量会被 v8 中的内存机制进行管理。

Chrome v8 创建的 cpp 数据类型能被我们编写的 cpp 代码(如cpp扩展)所访问,并且其与 js 中操作的是在内存中相同的存储单元。也就是说你在 js 中声明了一个 let a = 1;,那么相应作用域下,cpp代码能对其进行操作。

v8 创建的数据存储单元只能被它的内存回收机制所回收,而不能被我们自己所管理(不能被 delete或者 free)。回收的前提是——这个js变量在js代码中已经不被引用了,且在 cpp 代码中也不再被引用。

浮点数在 cpp 中通常是 float 或者 double;而在 v8 中则是 v8::Number,这也是个对象

作为一个 Node.js 开发者,大家可能对于老生代内存,新生代内存等耳熟能详。实际上 Chrome V8 中的堆内存却不止这两部分。

  • 新生态内存区:分配基础的数据对象,小而频。区域小但是垃圾回收频繁。
  • 老生代指针区:一堆指向老生代内存区具体数据内容的指针。新生代蜕变过来的对象会移动至此。
  • 老生代数据区:存放数据对象,而不是指向其他对象的指针。老生代指针区的指针就指向这里。
  • 大对象区:存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收并不会移动大对象。
  • 代码区:代码对象,也就是包含 JIT 之后指令的对象会分配到这里。唯一有执行权限的区域。
  • Cell 区、属性区、Map区:存放 Cell、属性 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单

1.新生代内存

使用 Scavenge 算法进行回收:将内存一分为二,一个是使用状态的 from 空间,一个是闲置状态的 to 空间。每次进行垃圾回收的时候都会有一半的内存是用不了的,但是由于其空间小,因此浪费不了多少空间。

如果有个对象在新生代的多次垃圾回收中都没被收走,那么他就会晋升到老生代内存,晋升的标准有:

  • 垃圾回收中,对象经历过一次新生代清理,就可以晋级了(类似于游戏晋级赛)
  • 垃圾回收过程中,如果To空间使用超过 25% ,那么它也可以晋级了(类似于补位晋级)

2.老生代内存

老生代保存一些周期长的对象,因此其占用内存非常多。

所以老生代就不会使用像新生代那样的算法了,会使用 Mark-SweepMark-Compact 的结合体。主要采用 Mark-Sweep 。如果老生代空间不足以分配从新生代晋级过来的对象时,才会使用 Mark-Compact

  1. Mark-Sweep(标记清除)

用于清除一些已死亡的对象。(标记v8内存中死亡的对象,然后清除掉)

  1. Mark-Compact(标记整理)

标记清除会产生内存碎片,而标记整理会在清除的基础上进行修改,在清除时让其变得更加紧缩。

  1. 惰性清理

在标记时,v8已经掌握了内存区对象的死活,他会延迟这一过程的清理。垃圾回收器可以根据自身需要来清理死掉的对象。

隔离实例

Chrome v8 中一个引擎实例的数据类型叫 Isolate,全称为隔离实例(Isolated Instance),是一个 v8 引擎的实例,也可以理解为引擎主体,每个实例内部拥有完全独立的各种状态:堆管理、垃圾回收机制。

抛开 Node.js 不说,使用 v8 进行开发的开发者其实是可以在它的程序中创建多个 Isolate 实例,并且并行地在多个线程中使用——但单个实例不能在多线程中使用。

在开发 node 的 cpp拓展时,已经就处于 v8 的环境中了,这时不需要再生成一个实例了,直接获取 node.js 环境所使用的实例即可:

1
2
3
4
5
void Method(const v8::FunctionCallbackInfo<v8::value>& args)
{
Isolate* isolate = args.GetIsolate();
// ...
}

上下文

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

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

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

脚本

是一段已经编辑好的 js 脚本的对象。数据类型就是 Script 。在编译是和一个处于活动状态的 上下文进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
v8::Local<v8::context> context = ...;

...

v8::Local<v8::String> source = 一段 js 代码;

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


// 执行脚本
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();

句柄(Handle)

句柄在 v8 中提供对于堆内存中的 js 数据对象的一个引用。

之所以使用句柄而不是对象指针之类的,一个主要的原因是因为 v8 在进行垃圾回收机制的时候,通常会将 js 的数据对象移来移去,如果使用指针的话,一个对象被移走了,那么这个指针就成为了野指针。如果是 handle ,垃圾回收器就会更新引用了数据块的那些 handle,让其断不了联系。

当一个对象不被句柄所引用时,就被认定为是垃圾。句柄的类型有:

  • 本地句柄(v8::Local)
  • 持久句柄(v8::Persistent)
  • 永生句柄(v8::Eternal)
  • 待实本地句柄
  • 其他句柄

其中本地和持久是最常用的句柄。

句柄的存在形式是 C++ 的一个模版类,其需要根据不同的 v8 数据类型进行不同的声明。

  • v8::Local<v8::Number>:本地 js 数值类型句柄
  • v8::Persistent<v8::String>:本地js字符串类型句柄

本地句柄

存在于栈内存中,在对应的析构函数被调用时被删除。生命周期是由其所在的句柄作用域所决定的。

本地句柄有一些比较重要的 api

1.创建(new)

New 是 Local 句柄的一个静态方法。在node v6.9.4 对应的 v8 版本有两个重载。

  • Local<T>::New(Isolate* isolate, Local<T> that): 传入Isolate 实例和一个本地句柄,进行复制构造
  • Local<T>::New(Isolate* isolate, const PersistentBase<T> &that):传入 Isolate 实例和一个持久句柄
2.清除(Clear)

将句柄的指向空:

1
2
Local<Number> handle = Number::New(isolate, 23333);
handle.Clear();
3.是否为空(IsEmpty)

判断为空并不是直接使用==,而是IsEmpty

1
2
3
4
5
Local<Number> handle = Number::New(isolate, 23333);

if (handle.IsEmpty()) {
// ...
}
4.转换数据类型(As/Cast)

将某种数据类型的句柄转换成另外一种类型的本地句柄,可以使用AsCast 函数