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.js
、 Lode.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-Sweep
和 Mark-Compact
的结合体。主要采用 Mark-Sweep
。如果老生代空间不足以分配从新生代晋级过来的对象时,才会使用 Mark-Compact
。
- Mark-Sweep(标记清除)
用于清除一些已死亡的对象。(标记v8内存中死亡的对象,然后清除掉)
- Mark-Compact(标记整理)
标记清除会产生内存碎片,而标记整理会在清除的基础上进行修改,在清除时让其变得更加紧缩。
- 惰性清理
在标记时,v8已经掌握了内存区对象的死活,他会延迟这一过程的清理。垃圾回收器可以根据自身需要来清理死掉的对象。
隔离实例
Chrome v8 中一个引擎实例的数据类型叫 Isolate
,全称为隔离实例(Isolated Instance
),是一个 v8 引擎的实例,也可以理解为引擎主体,每个实例内部拥有完全独立的各种状态:堆管理、垃圾回收机制。
抛开 Node.js 不说,使用 v8 进行开发的开发者其实是可以在它的程序中创建多个 Isolate 实例,并且并行地在多个线程中使用——但单个实例不能在多线程中使用。
在开发 node 的 cpp拓展时,已经就处于 v8 的环境中了,这时不需要再生成一个实例了,直接获取 node.js 环境所使用的实例即可:
1 | void Method(const v8::FunctionCallbackInfo<v8::value>& args) |
上下文
上下文对象是用来定义 js 执行环境的一个对象,其数据类型是 Context
,它在创建的时候要指明属于哪个实例:
1 | v8::Isolate* isolate = ...; |
这里可以理解为一个沙箱化的执行上下文环境,内部预置了一系列对象和函数。
脚本
是一段已经编辑好的 js 脚本的对象。数据类型就是 Script
。在编译是和一个处于活动状态的 上下文进行绑定:
1 | v8::Local<v8::context> context = ...; |
句柄(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 | Local<Number> handle = Number::New(isolate, 23333); |
3.是否为空(IsEmpty)
判断为空并不是直接使用==
,而是IsEmpty
1 | Local<Number> handle = Number::New(isolate, 23333); |
4.转换数据类型(As/Cast)
将某种数据类型的句柄转换成另外一种类型的本地句柄,可以使用As
和Cast
函数