本系列内容基于我对 v8 源码的 debug 和个人理解, 如果有错误请在评论区指正, 感谢
前言
v8
的源码比较大, 涉及到的概念也很多, 万事开头难, 所以阅读源码需要有耐心
本文重点在介绍 v8
内存初始化的过程和 v8
的一些基本概念
环境搭建
v8 官网有详细的文档介绍如何下载编译,还有IDE
环境搭建
V8 的初始化
通过 hello-world
看 v8
的初始化
代码在 samples/hello-world.cc
里面, 我把比较重要的内容提取出来
// 初始化一个平台
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
// v8 初始化
v8::V8::Initialize();
// isolate 初始化
v8::Isolate* isolate = v8::Isolate::New(create_params);
// 创建一个 HandleScope
v8::HandleScope handle_scope(isolate);
// 要执行的 JS 代码
v8::Local<v8::String> source = v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'");
// 编译代码
v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();
// 执行代码
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
总结一下
如果要初始化并运行 js
代码, v8
需要做下面的事情
- platform 初始化
- V8 初始化
- Isolate 初始化
- 创建一个 HandleScope
- 创建
JS
代码 - 编译
JS
代码 - 执行
JS
代码
下面一个个介绍
Platform 初始化
Platform
初始化的时候会启动线程池, 并绑定out of memory
的钩子
因为没做什么特殊的事情, 我就一笔带过了
cppgc::internal::GetGlobalOOMHandler().SetCustomHandler(
&GlobalFatalOutOfMemoryHandlerImpl);
V8 初始化
v8
初始化做了很多事情
比较重要的是 Sandbox
的初始化
Sandbox
套用v8 blog 里面的一句话来解释 Sandbox
的作用
memory corruption cannot "spread" to other parts of the process' memory.
大致含义是使用 Sandbox
可以避免内存越界访问造成的崩溃, 下面会详细解释
先看下 Sanbox
的初始化代码
分配内存
首先判断Sandbox
最大能分配的内存空间
这里的注释写的很清楚
如果是 40位
系统, 用户空间能分配的最大内存空间是 512G
, Sanbox
最多分配用户空间的 1/4, 也就是 128G
,这个对应 Sanbox
最多能分配的虚拟内存空间
而 Sanbox
需要的虚拟内存空间是1TB
, 最终分配的虚拟内存是这两者的最小值
因为现在大部分都是64位
系统,对应用户空间 128 TB
, 1/4 为 32TB
, 远远大于 1TB
, 所以 Sanbox
会分配 1TB
的虚拟内存空间
// kSandboxSize = 1TB
size_t reservation_size = std::min(kSandboxSize, max_reservation_size)
虚拟内存在实际访问的时候才分配, 即便你物理内存没有
1TB
也是可以分配的哈
在实际分配的时候, 会在前后各加 32G
内存作为保护, 实际分配的虚拟内存就是1TB + 64GB
内存的分布如下所示
左右加上 32GB
是为了避免越界访问, 这就跟上面提到的 Sandbox
的作用有关系了
因为 v8
只会使用从 base
到 end
内存空间, 如果越界访问到了前后的 32GB
里面, 就可以拦截下来, 而不至于导致崩溃, 起到一个保护的作用
之后所有的js
对象和一些数据结构都会分配在 base
到 end
这一段内存空间中
IsolateGroup
上面预留了空间之后, v8
会继续初始化 IsolateGroup
顾名思义, IsolateGroup
管理所有的 Isolate
, Isolate
后面会提到, Group
主要做下面的事情
sandbox->address_space()->AllocatePages(
base, // sandbox 的 base 地址
reservation_size, // 4GB
alignment,
PagePermissions::kNoAccess
);
IsolateGroup
在 Sandbox
的基础上划走 4GB
内存空间,用来分配 js 对象
因为v8
使用了压缩指针的技术, js
对象的指针都是 32 位
, 所以只需要 4GB
就够了
至于什么是压缩指针, 以后会单独写一篇介绍
Isolate
Isolate 是 v8
执行的独立环境。每个 isolate
都会有自己的状态,互不干扰。
每次创建js
对象,都是从 Isolate
开始分配的, 也就是说Isolate
管理了所有的js
对象分配和GC
是运行 js
最重要的概念之一, 关于里面Heap
的部分, 以后会补充
比如创建一个 String
Local<String> str = v8::String::NewFromUtf8Literal(isolate, "string");
里面的调用是这样的
isolate->factory()->NewStringFromUtf8(string)
而更底层的调用在 src/heap
里面, 而这些内容都是 Isolate
来管理
AllocateRaw(
size, // 大小
allocation, // 分配类型, 新生代,老生代
alignment // 怎么对齐
)
具体的代码可以查看 src/execution/isolate.cc
, 很多内容我也没看到,后面有用到的地方再来补充
HandleScope 和 Handle
HandleScope, Handle
是 v8
为了管理资源而设计的
RAII
先解释下 cpp
的 RAII
才能更好的理解 HandleScope
和 Handle
很熟悉 cpp 的话可以绕过
#define print(x) std::cout << x << std::endl;
class Scope {
public:
Scope()
{
print("enter scope");
}
~Scope()
{
print("leave scope");
}
};
上面的 Scope
, 初始化的时候会打印enter scope
, 离开作用域的时候会被清理(析构), 析构的时候打印leave scope
int main()
{
{
Scope scope;
// 在这里离开作用域
}
print("exit");
}
上面的代码会打印
enter scope
leave scope
exit
而下面的代码
int main()
{
Scope scope;
print("exit");
// 在这里离开作用域
}
输出
enter scope
exit
leave scope
也就是说在 { } 结束的时候自动析构, 释放资源, RAII
是一种管理资源的方式
HandleScope
看下面的代码, 在{}
结束的时候, 根据 RAII 策略会自动的释放 HandleScope
, 同时释放在这个 block
内所有 v8
创建的 js 对象
然后 v8
可以通过GC
清理掉对应的资源
{
// 创建一个 HandleScope
v8::HandleScope handle_scope(isolate);
// 创建一个 Local<String>
v8::Local<v8::String> source = v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'");
// 在这里 HandleScope 析构
}
重点要看下 HandleScope
的初始化和析构是怎么实现的, 下面就直接放代码了
上面说到了 Isolate
存储了执行 js
的所有环境信息, 其中就包含 HandleScopeData
HandleScope
创建的时候会更新 isolate->handle_scope_data_
源代码在 api/api.cc
首先获取 isolate
的 handle_scope_data
, 对 current->next
做了一个记录(pre_next_
), 后面HandleScope
析构的时候还原回来
current->next
里面是一个数组指针 Address*
v8 里面指针类型就是 Address
也就是说 handle_scope_data
里面存的主要是一个数组, 数组里面都是指针(Address)
当析构的时候, HandleScope
会去清理掉 HandleScope 所有的指针, 详细代码在 handles/handles-inl.h
清理的方式有两种
- 给这些指针都标记为一个数值
0x1baddead0baddeaf
, 这样如果发生了错误的访问, v8 能知道这个指针已经失效了 - 啥都不做, 只是把
handle_scope_data_->next
还原到pre_next_
, 这样也算清理了, Address 无法被访问
下次 GC
的时候无法访问到这些 Address 指针
, 就会清理掉它们
总结一下
HandleScope
创建的时候在 isolate -> handle_scope_data_
里面记录一个Address 数组
每当有数据在 isolate
里面创建的时候, 就会更新 Address数组
, 这样 GC
就不会清理这些数据
等到析构的时候, 把这些指针从数组里面推出, GC
无法访问到这些指针,就会释放它们
Handle
从一个 Local<String>
的创建来说 Handle
Local<String> script = String::NewFromUtf8Literal(isolate, "let x = 1");
String
的创建不是这里的重点,所以直接到 Handle
被初始化的地方
底层调用了 isolate->factory()->NewRawStringWithMap
来创建一个 String
类型,重点在 handle(string,isolate())
继续向里面跳
Handle
继承 HandleBase
, 里面调用了 HandleScope::CreateHandle
方法
CreateHandle
在 HandleScopeData
的指针数组里面新增了一个指针
总结一下
当创建 Handle
的时候, 会在 isolate->handle_scope_data_
里面增加一个指针Address
上面说过HandleScope
销毁的时候会清理掉这些指针,对应的数据也就释放了
值得注意的是对 Handle
解引用,会得到 Tagged<T>
, 这个后面讲压缩指针的时候会提到
Local
Local
是 v8
对外输出的数据结构, 里面只有一个指针, Local
继承 LocalBase
内部有可能是Handle<T>
转换成 Local<T>
对外统一暴露 Local<T>
// v8 统一对外输出 Local<T>
Local<Boolean> a = Boolean::New(isolate, true);
Local<Number> b = Number::New(isolate, 10);
Local<String> c = String::NewFromUtf8Literal(isolate, "c");
总结
上面大致讲解了 v8
的初始化流程
从 Sandbox
开始向操作系统分配内存
IsolateGroup
在 Sandbox
的基础上分配了 4G
内存
Isolate
在这个基础上管理对象分配
和gc
剩下的 HandleScope
和 Handle
都是 v8
常用的基本概念
本人能力有限, 如果有理解错误的地方可以在评论区指出