本文主要参考 https://github.com/danbev/learning-v8
Intruction
V8 基本上由堆的内存管理和执行堆栈组成(简化助于说明。 回调队列、事件循环和 WebAPI(DOM、ajax、setTimeout 等)之类的东西可以在 Chrome 中找到,或者在 Node 的情况下,API 是 Node.js API:
1 | +------------------------------------------------------------------------------------------+ |
执行栈是一个帧指针栈。 对于每个调用的函数,该函数将被压入堆栈。 当该函数返回时,它将被删除。
如果该函数调用其他函数,它们将被压入堆栈。 当它们都返回后,可以从返回的点继续执行。
如果其中一个函数执行需要时间的操作,则在完成之前不会进行,因为完成的唯一方法是该函数返回并从堆栈中弹出。
当您拥有单线程编程语言时,就会发生这种情况。
这样就描述了同步函数,那么异步函数呢?
以调用 setTimeout 为例,setTimeout 函数将被压入调用堆栈并执行。 这就是回调队列和事件循环发挥作用的地方。 setTimeout 函数可以将函数添加到回调队列中。 当调用堆栈为空时,该队列将由事件循环处理。
Task
任务是可以通过将任务放在回调队列中来调度的函数。 这是由诸如 setTimeout 和 setInterval 之类的 WebAPI 完成的。
当事件循环开始执行任务时,它将运行当前在任务队列中的所有任务。 任何由 WebAPI
函数调用调度的新任务只会被推送到队列中,但在事件循环的下一次迭代之前不会执行。
当执行堆栈为空时,微任务队列中的所有任务都将运行,如果这些任务中的任何一个将任务添加到微任务队列中,微任务队列也会
运行,这与任务队列处理这种情况的方式不同。
在 Node.js 中的 setTimeout 和 setInterval…
Micro task
是在当前函数在当前调用堆栈上的所有其他函数之后运行之后执行的函数。微任务内部信息可以在中找到。
Microtask queue
当一个promise被创建时,它会立即执行,如果它已经被reovled,你可以调用它。
Template
这是对象模板 和 函数模板的super类,在javascript中,函数像对象一样拥有字段
例子
1 | class V8_EXPORT Template : public Data {//继承Data类 |
Set 函数是设置使用此模板创建实例的名称和值。 SetAccessorProperty 用于使用函数获取/设置的属性。
PropertyAttribute和AccessControl定义如下
1 | enum PropertyAttribute { |
ObjectTemplate
在没有专用构造函数的情况下创建 JavaScript 对象如下。 当使用 ObjectTemplate 创建实例时,实例将具有在
ObjectTemplate 上配置的属性和功能。
如下:
1 | const obj = {}; |
此类在include/v8.h中申明并扩展了模板
1 | class V8_EXPORT ObjectTemplate : public Template { |
我们创建一个 ObjectTemplate 实例,我们可以向它添加使用此 ObjectTemplate 实例创建的所有实例都将具有的属性。
这是通过调用模板类的成员 Set 来完成。 我们可以为属性指定一个 Local。 Name 是 Symbol 和 String 的超类,
它们都可以用作属性的名称。Set 的实现可以在 src/api/api.cc 中找到:
1 | void Template::Set(v8::Local<Name> name, v8::Local<Data> value, v8::PropertyAttribute attribute) { |
另一个例子
1 |
|
FunctionTempate
用来创建函数的模板与objecttempate一样继承自template
1 | class V8_EXPORT FunctionTemplate : public Template { |
javascript中函数可以像对象一样拥有属性,创建函数模板方法如下:
1 |
|
创建函数模板的示例的方法如下:
1 | Local<FunctionTemplate> ft = FunctionTemplate::New(isolate_, function_callback, data); |
并使用一下方法调用该函数:
1 | MaybeLocal<Value> ret = function->Call(context, recv, 0, nullptr); |
Function call在src/api/api.cc:
1 | bool has_pending_exception = false; |
call的返回值是一个MaybeHandle
1 | template <class T> |
这时看一下Execution::call,位于execution/execution.cc中,它调用:
1 | return Invoke(isolate, InvokeParams::SetUpForCall(isolate, callable, receiver, argc, argv)); |
SetUpForCall 将返回一个 InvokeParams。(仔细看看InvokeParams)
1 | V8_WARN_UNUSED_RESULT MaybeHandle<Object> Invoke(Isolate* isolate, |
1 | Handle<Object> receiver = params.is_construct |
在我们的例子中 is_construct 为false,因为我们没有使用 new 和接收器,函数中的 this 应该设置为我们传入的接收器。之后我们有
Builtins::InvokeApiFunction
1 | auto value = Builtins::InvokeApiFunction( |
1 | result = HandleApiCallHelper<false>(isolate, function, new_target, |
api-arguments-inl.h 有:
1 | FunctionCallbackArguments::Call(CallHandlerInfo handler) { |
对 f(info) 的调用是调用回调的方法,这只是一个普通的函数调用。回到 HandleApiCallHelper 有:
1 | Handle<Object> result = custom.Call(call_data); |
RETURN_EXCEPTION_IF_SCHEDULED_EXCEPTION 扩展为:
1 | Handle<Object> result = custom.Call(call_data); |
注意,如果出现异常,则返回一个空对象。 稍后在 execution.cca 中调用:
1 | auto value = Builtins::InvokeApiFunction( |
Address
Address在include/v8-internal.h:
1 | typedef uintptr_t Address; |
uintptr_t 是 cstdint 中指定的可选类型,能够存储数据指针。 它是一种无符号整数类型,任何指向 void
的有效指针都可以转换为这种类型(并返回)。
TaggedImpl
此类在 src/objects/tagged-impl.h 中声明,并有一个私有成员,声明为:
1 | public |
可以使用以下方法创建实例:
1 | i::TaggedImpl<i::HeapObjectReferenceType::STRONG, i::Address> tagged{}; |
存储类型也可以是 globals.h 中定义的 Tagged_t:
1 | using Tagged_t = uint32_t; |
使用指针压缩时,它看起来可能是不同的值。
Object
这个类扩展了TaggedImpl:
1 | class Object : public TaggedImpl<HeapObjectReferenceType::STRONG, Address> { |
可以使用默认构造函数创建对象,也可以通过传入 TaggedImpl 构造函数的地址来创建对象。
对象本身没有任何成员(除了从 TaggedImpl 继承的 ptr_ )。
因此,如果我们在堆栈上创建一个对象,这就像一个对象的指针/引用:
1 | +------+ |
现在, ptr_ 是一个 储存类型 所以它可能是一个 Smi 在这种情况下它会直接包含值,例如一个小整数:
1 | +------+ |
ObjectSlot
1 | i::Object obj{18}; |
1 | +----------+ +---------+ |
Maybe
Maybe像一个可选项,它可以保存一个值,也可以什么都不保存
1 | template <class T> |
关于Maybe的理解如下:
1 | bool cond = true; |
有一些函数可以检查 Maybe 是否为空,如果是则使进程崩溃。 还可以使用 FromJust 检查并返回该值。
Maybe 的使用是 api 调用可能失败的地方,返回 Nothing 是一种表示这一点的方式。
MaybeLocal
1 | template <class T> |
如果 val_ 是 nullptr,ToLocalChecked 将使进程崩溃。 如果想避免崩溃,可以使用 ToLocal。
Data
Data是所有可以存在v8堆的对象的超类
1 | class V8_EXPORT Data { |
Value
Value 扩展了 Data 并添加了许多方法来检查 Value 是否属于某种类型,例如 IsUndefined()、IsNull、IsNumber
等。它还具有转换为 Local 的有用方法,例如:
1 | V8_WARN_UNUSED_RESULT MaybeLocal<Number> ToNumber(Local<Context> context) const; |
Handle
Handle 与 Object 和 ObjectSlot 的相似之处在于它也包含一个 Address 成员(称为 location_ 并在 HandleBase
中声明),但不同之处在于 Handle 充当抽象层并且可以由垃圾收集器重新定位。 可以在 src/handles/handles.h 中找到。
1 | class HandleBase { |
1 | +----------+ +--------+ +---------+ |
1 | (gdb) p handle |
注意 location_ 包含一个指针:
1 | (gdb) p /x *(int*)0x7ffdf81d60c0 |
这与 obj 中的值相同:
1 | (gdb) p /x obj.ptr_ |
我们可以使用任何指针访问 int:
1 | (gdb) p /x *value |
HandleScope
包含许多本地/句柄(认为指向对象的指针,但由 V8 管理)并将负责为我们删除本地/句柄。 HandleScopes 是堆栈分配的
当 ~HandleScope 被调用时,在该范围内创建的所有句柄都将从 HandleScope 维护的堆栈中删除,
这使得句柄指向的对象有资格被 GC 从堆中删除。
HandleScope 只有三个成员:
1 | internal::Isolate* isolate_; |
让我们仔细看看构造 HandleScope 时会发生什么:
1 | v8::HandleScope handle_scope{isolate_}; |
构造函数调用将在 src/api/api.cc 中结束,构造函数只是委托给 Initialize:
1 | HandleScope::HandleScope(Isolate* isolate) { Initialize(isolate); } |
每个 v8::internal::Isolate 都有 HandleScopeData 类型的成员:
1 | HandleScopeData* handle_scope_data() { return &handle_scope_data_; } |
HandleScopeData 是在 src/handles/handles.h 中定义的结构:
1 | struct HandleScopeData final { |
请注意,有两个指向下一个的指针 (Address* ) 和一个 limit。 当 HandleScope 初始化时,当前的 handle_scope_data
将从内部隔离中检索。 正在创建的 HandleScope 实例存储当前隔离的下一个/限制指针,以便在关闭此 HandleScope
时可以恢复它们(请参阅 CloseScope)。
那么在创建了 HandleScope 之后,Local 如何与这个实例交互呢?
当创建一个 Local 时,这将/可能通过 FactoryBase::NewStruct 它将分配一个新的 Map 然后为正在创建的 InstanceType
创建一个句柄:
1 | Handle<Struct> str = handle(Struct::cast(result), isolate()); |
这将落在构造函数 Handlesrc/handles/handles-inl.h
1 | template <typename T> |
请注意,object.ptr() 用于将地址传递给 HandleBase。 还要注意 HandleBase 将其 location_ 设置为
HandleScope::GetHandle 的结果。
1 | Address* HandleScope::GetHandle(Isolate* isolate, Address value) { |
在这种情况下将调用 CreateHandle,并且此函数将检索当前隔离的 handle_scope_data:
1 | HandleScopeData* data = isolate->handle_scope_data(); |
在这种情况下,next 和 limit 都将为 0x0,因此将调用 Extend。 Extend 还将获取隔离物 handle_scope_data
并检查当前级别,然后获取隔离物 HandleScopeImplementer:
1 | HandleScopeImplementer* impl = isolate->handle_scope_implementer(); |
HandleScopeImplementer 在 src/api/api.h 中声明
HandleScope:CreateHandle 将从隔离中获取handle_scope_data:
1 | Address* HandleScope::CreateHandle(Isolate* isolate, Address value) { |
请注意,data->next 设置为传入的地址 + 地址的大小。
HandleScope 的析构函数将调用 CloseScope。 有关示例,请参见 handlescope_test.cc。
EscapableHandleScope
本地句柄位于堆栈上,并在调用适当的析构函数时被删除。 如果有一个本地
HandleScope,那么它会在范围返回时处理这个问题。 当没有对句柄的引用时,它可以被垃圾收集。
这意味着如果一个函数有一个 HandleScope 并且想要返回一个句柄/本地,那么它在函数返回后将不可用。 这就是
EscapableHandleScope 的用途,它使值能够被放置在封闭的句柄范围中以允许它生存。 当封闭的 HandleScope
超出范围时,它将被清理。
1 | class V8_EXPORT EscapableHandleScope : public HandleScope { |
来自 api.cc
1 | EscapableHandleScope::EscapableHandleScope(Isolate* v8_isolate) { |
因此,当创建 EscapableHandleScope 时,它将创建一个带the_hole_value的句柄并将其存储在地址类型的 escape_slot_ 中。
这个Handle 将在当前的 HandleScope 中创建,EscapableHandleScope 稍后可以为它想要转义的指针/地址设置一个值。 稍后当
HandleScope 超出范围时,它将被清理。 然后它像普通的 HandleScope 一样调用 Initialize。
1 | i::Address* HandleScope::CreateHandle(i::Isolate* isolate, i::Address value) { |
来自handles-inl.h:
1 | Address* HandleScope::CreateHandle(Isolate* isolate, Address value) { |
当调用 Escape 时,会发生以下情况(v8.h):
1 | template <class T> |
在 EscapeableHandleScope::Escape (api.cc):
1 | i::Address* EscapableHandleScope::Escape(i::Address* escape_value) { |
如果 escape_value 为 null,则作为指向父 HandleScope 的指针的 escape_slot 被设置为 undefined_value()
而不是之前的洞值,并且 nullptr 将被返回。 这个返回的地址/指针将在转换为 T* 后返回。 接下来,我们看看当
EscapableHandleScope 超出范围时会发生什么。 这将调用 HandleScope::~
HandleScope,这很有意义,因为应该清理任何其他本地句柄。
Escape 将其参数的值复制到封闭范围中,删除所有本地句柄,然后返回可以安全返回的新句柄副本。
HeapObject
待补充
Local
有一个成员 val_,它是指向 T 的类型指针:
1 | template <class T> class Local { |
请注意,这是一个指向 T 的指针。我们可以使用以下命令创建一个本地:
1 | v8::Local<v8::Value> empty_value; |
所以 Local 包含一个指向类型 T 的指针。我们可以使用 operator-> 和 operator* 访问这个指针。
我们可以使用 Local::Cast 从子类型转换为超类型:
1 | v8::Local<v8::Number> nr = v8::Local<v8::Number>(v8::Number::New(isolate_, 12)); |
而且还有
1 | v8::Local<v8::Value> val2 = nr.As<v8::Value>(); |
PrintObject
使用 C++ 中的 _ v8_ internal_Print_Object:
1 | $ nm -C libv8_monolith.a | grep Print_Object |
请注意,此函数没有命名空间。 我们可以将其用作:
1 | extern void _v8_internal_Print_Object(void* object); |
让我们仔细看看上面的内容:
1 | v8::internal::Object** gl = ((v8::internal::Object**)(*global)); |
我们使用解引用运算符来获取 Local (* global) 的值,它只是 T* 类型,一个指向 Local 类型的指针:
1 | template <class T> |
然后我们将其转换为指向 Object 的指针类型。
1 | gl** Object* Object |
v8::internal::Object 的一个实例只有一个数据成员,它是一个名为 ptr_ 类型为 Address 的字段:
src/objects/objects.h:
1 | class Object : public TaggedImpl<HeapObjectReferenceType::STRONG, Address> { |
让我们看一下其中一个功能,看看它是如何实现的。 例如在 OBJECT_TYPE_LIST 我们有:
1 |
|
所以对象类将有一个看起来像这样的函数:
1 | inline bool IsNumber() const; |
在 src/objects/objects-inl.h 我们将有实现:
1 | bool Object::IsNumber() const { |
IsHeapObject 在 TaggedImpl 中定义:
1 | constexpr inline bool IsHeapObject() const { return IsStrong(); } |
该宏可以在 src/common/globals.h 中找到:
1 |
|
因此,我们将地址类型的 ptr_ 转换为 src/common/global.h 中定义的 Tagged_t
类型,并且可以根据是否使用压缩指针而有所不同。 如果它们不受支持,则与地址相同:
1 | using Tagged_t = Address; |
src/objects/tagged-impl.h:
1 | template <HeapObjectReferenceType kRefType, typename StorageType> |
HeapObjectReferenceType 可以是 WEAK 或 STRONG。 在这种情况下,存储类型是地址。 所以 Object
本身只有一个成员,它是从其唯一的超类继承的,这就是 ptr_。
所以下面是告诉编译器将我们的 Local,* global 的值视为一个指针(它已经是)指向一个指向一个内存位置的指针,该内存位置遵循
v8::internal::Object 的布局 类型,我们现在知道它有一个 prt_ 成员。 我们想取消引用它并将其传递给函数。
1 | _v8_internal_Print_Object(*((v8::internal::Object**)(*global))); |
ObjectTemplate
但是我仍然缺少 ObjectTemplate 和对象之间的联系。 当我们创建它时,我们使用:
1 | Local<ObjectTemplate> global = ObjectTemplate::New(isolate); |
在 src/api/api.cc 我们有:
1 | static Local<ObjectTemplate> ObjectTemplateNew( |
在这种情况下,什么是结构?
src/objects/struct.h
1 |
|
请注意,包含指定扭矩生成的包含,可以在 /x64.release_gcc/gen/torque-generated/class-definitions-tq 中找到。
因此,在编译主源文件之前,必须在某处调用torque executable,该可执行文件生成代码存根汇编程序 C++ 头文件和源代码。 在
Building V8 中有并且有一个关于此的部分。
宏 TQ_OBJECT_CONSTRUCTORS 可以在 src/objects/object-macros.h 中找到并扩展为:
1 | constexpr Struct() = default; |
那么 TorqueGeneratedStruct 是什么样的呢?
1 | template <class D, class P> |
在这种情况下,其中 D 是 Struct 而 P 是 HeapObject。 但是上面是类型的声明,但是我们在 .h 文件中拥有的是生成的内容。
这种类型在 src/objects/struct.tq 中定义:
1 | @abstract |
NewStruct 可以在 src/heap/factory-base.cc 中找到
1 | template <typename Impl> |
存储在 v8 堆上的每个对象都有一个 Map (src/objects/map.h),用于描述所存储对象的结构。
1 | class Map : public HeapObject { |
1 | 1725 return Utils::ToLocal(obj); |
所以这就是连接,我们看到的Local是一个HandleBase。
1 | (lldb) expr gl |
您可以使用以下命令重新加载 .lldbinit:
1 | (lldb) command source ~/.lldbinit |
这在调试 lldb 命令时很有用。 您可以在该位置设置断点并中断并更新命令并重新加载,而无需重新启动 lldb。
目前,v8 附带的 lldb-commands.py 包含对传递给 ptr_arg_cmd 的参数的额外操作:
1 | def ptr_arg_cmd(debugger, name, param, cmd): |
请注意,param 是我们要打印的对象,例如,假设它是一个名为 obj 的本地对象:
1 | param = "(void*)(obj)" |
然后这将被“传递”/格式化为命令字符串:
1 | "_v8_internal_Print_Object(*(v8::internal::Object**)(*(void*)(obj))") |
Threads
V8 是单线程的(堆栈功能的执行),但有支持线程用于垃圾收集、分析(IC,也许还有其他东西)(我认为)。 让我们看看有哪些线程:
1 | $ LD_LIBRARY_PATH=../v8_src/v8/out/x64.release_gcc/ lldb ./hello-world |
所以在启动时只有一个线程,这是我们所期望的。 让我们跳到我们创建平台的地方:
1 | Platform* platform = platform::CreateDefaultPlatform(); |
接下来检查 0,处理器数量 -1 用作线程池的大小:
1 | (lldb) fr v thread_pool_size |
这就是 SetThreadPoolSize 所做的一切。 在此之后,我们有:
1 | platform->EnsureInitialized(); |
new Worker Thread 将创建一个新的 pthread:
1 | result = pthread_create(&data_->thread_, &attr, ThreadEntry, this); |
可以在 src/base/platform/platform-posix 中找到 ThreadEntry
International Component for Unicode (ICU)
Unicode 国际组件 (ICU) 处理国际化 (i18n)。 ICU 提供支持区域设置敏感的字符串比较、日期/时间/数字/货币格式等。
有一个名为 ECMAScript 402 的可选 API,V8 支持并默认启用。 i18n-support 表示即使您的应用程序不使用 ICU,您仍然需要调用
InitializeICU :
1 | V8::InitializeICU(); |
Local
1 | Local<String> script_name = ...; |
script_name是一个由 v8 GC 管理的对象引用。 GC 需要能够移动事物(指针)并跟踪事物是否应该被 GC
与持久句柄相比,本地句柄是轻量级的,并且主要使用本地操作。 这些句柄由 HandleScopes
管理,因此您必须在堆栈上有一个句柄范围,并且本地仅在句柄范围有效时才有效。 这使用资源获取即初始化 (RAII),因此当
HandleScope 实例超出范围时,它将删除所有本地实例。
Local 类(在 include/v8.h 中)只有一个成员,其类型为指向类型 T 的指针。所以对于上面的示例,它将是:
1 | String* val_; |
您可以在 include/v8.h 中找到 Local 的可用操作。
1 | (lldb) p script_name.IsEmpty() |
Local 重载了许多运算符,例如 ->:
1 | (lldb) p script_name->Length() |
其中 Length 是 v8 String 类的一个方法。
句柄堆栈不是 C++ 调用堆栈的一部分,但句柄范围嵌入在 C++ 堆栈中。 句柄范围只能堆栈分配,不能用 new 分配。
Persistent
https://v8.dev/docs/embed:持久句柄提供对堆分配的 JavaScript 对象的引用,就像本地句柄一样。有两种风格,它们处理的引用的生
命周期管理不同。当您需要为多个函数调用保留对对象的引用时,或者当句柄生存期与 C++
范围不对应时,请使用持久句柄。例如,谷歌浏览器使用持久句柄来引用文档对象模型 (DOM) 节点。
可以使用 PersistentBase::SetWeak 将持久句柄设为弱,以在对对象的唯一引用来自弱持久句柄时触发垃圾收集器的回调。
UniquePersistent 句柄依赖于 C++ 构造函数和析构函数来管理底层对象的生命周期。 Persistent
可以使用其构造函数构造,但必须使用 Persistent::Reset 显式清除。
那么持久对象是如何创建的呢?让我们编写一个测试并找出(test/persistent-object_text.cc):
1 | $ make test/persistent-object_test |
现在,要创建 Persistent 的实例,我们需要一个 Local 实例,否则 Persistent 实例将是空的。
1 | Local<Object> o = Local<Object>::New(isolate_, Object::New(isolate_)); |
Local
1 | Local<v8::Object> v8::Object::New(Isolate* isolate) { |
发生的第一件事是公共 Isolate 指针被强制转换为指向内部 Isolate 类型的指针。 LOG_API 是同一源文件 (src/api/api.cc)
中的宏:
1 |
|
如果我们的情况,预处理器会将其扩展为:
1 | i::RuntimeCallTimerScope _runtime_timer( |
LOG 是一个宏,可以在 src/log.h 中找到:
1 |
|
这将扩展到:
1 | v8::internal::Logger* logger = isolate->logger(); |
因此,随着 LOG_API 宏的扩展,我们有:
1 | Local<v8::Object> v8::Object::New(Isolate* isolate) { |
接下来我们有 ENTER_V8_NO_SCRIPT_NO_EXCEPTION:
1 |
|
因此,随着宏的扩展,我们有:
1 | Local<v8::Object> v8::Object::New(Isolate* isolate) { |
首先,我调用了isolate->object function() 并将结果传递给NewJSObject。 object_function 由名为 NATIVE_CONTEXT_FIELDS
的宏生成:
1 |
|
NATIVE_CONTEXT_FIELDS 是 src/contexts 中的一个宏
1 |
|
1 | Handle<type> Isolate::object_function() { |
我不清楚不同类型的上下文,有一个本地上下文,一个“正常/公共”上下文。 在 src/contexts-inl.h 我们有 native_context 函数:
1 | Context* Context::native_context() const { |
Context 扩展了 FixedArray,因此 get 函数是 FixedArray 的 get 函数,而 NATIVE_CONTEXT_INDEX
是存储本机上下文的数组的索引。现在,让我们仔细看看 NewJSObject。
如果在 src/heap/factory.cc 中搜索 NewJSObject:
1 | Handle<JSObject> Factory::NewJSObject(Handle<JSFunction> constructor, PretenureFlag pretenure) { |
NewJSObjectFromMap
1 | ... |
所以我们创建了一张新map
Map
HeapObject 包含指向 Map 的指针,或者更确切地说,具有返回指向 Map 的指针的函数。 我在HeapObject
类中看不到任何成员映射。让我们看看何时创建map。
1 | (lldb) br s -f map_test.cc -l 63 |
1 | Handle<Map> Factory::NewMap(InstanceType type, |
我们可以看到上面是在堆实例上调用 AllocateRawWithRetryOrFail,传递一个大小为 88 并指定 MAP_SPACE:
1 | HeapObject* Heap::AllocateRawWithRetryOrFail(int size, AllocationSpace space, |
对齐的默认值为 kWordAligned。 阅读标题中的文档,它说此函数将尝试在 MAP_SPACE 中执行大小为 88
的分配,如果失败,将执行完整的 GC 并重试分配。 让我们看一下 AllocateRawWithLigthRetry:
1 | AllocationResult alloc = AllocateRaw(size, space, alignment); |
AllocateRaw 可以在 src/heap/heap-inl.h 中找到。 根据空间参数,将采用不同的路径。 由于在我们的例子中是 MAP_
SPACE,我们将专注于该路径:
1 | AllocationResult Heap::AllocateRaw(int size_in_bytes, AllocationSpace space, AllocationAlignment alignment) { |
map_space_ 是 Heap 的私有成员(src/heap/heap.h):
1 | MapSpace* map_space_; |
AllocateRawUnaligned 可以在 src/heap/spaces-inl.h 中找到:
1 | AllocationResult PagedSpace::AllocateRawUnaligned( int size_in_bytes, UpdateSkipList update_skip_list) { |
update_skip_list 的默认值为 UPDATE_SKIP_LIST。 因此,让我们看一下 AllocateLinearly:
1 | HeapObject* PagedSpace::AllocateLinearly(int size_in_bytes) { |
回想一下,在我们的例子中 size_in_bytes 是 88。
1 | (lldb) expr current_top |
请注意,首先将 top 设置为 new_top ,然后返回 current_top ,这将是指向内存中对象开始的指针(在本例中是
v8::internal::Map ,它也是 HeapObject 类型 )。 我一直想知道为什么 Map(和其他 HeapObject)没有任何成员字段,并且只有/
主要是组成对象的各种字段的 getter/setter。 答案是指向例如 Map 实例的指针指向实例的第一个内存位置。 getter/setter
函数使用索引来读取/写入内存位置。 索引大多以枚举字段的形式定义类型的内存布局。
接下来,在 AllocateRawUnaligned 中,我们有 MSAN_ALLOCATED_UNINITIALIZED_MEMORY 宏:
1 | MSAN_ALLOCATED_UNINITIALIZED_MEMORY(object->address(), size_in_bytes); |
MSAN_ALLOCATED_UNINITIALIZED_MEMORY 可以在 src/msan.h 中找到,ms 代表 Memory Sanitizer,只有在定义了 V8_US_MEMORY_
SANITIZER 时才会使用。 返回的对象将用于在返回时构造一个 AllocationResult。 回到 AllocateRaw 我们有:
1 | if (allocation.To(&object)) { |
这将在 AllocateRawWithLightRetry 中返回我们:
1 | HeapObject* result = AllocateRawWithLigthRetry(size, space, alignment); |
并且该返回将返回到 src/heap/factory.cc 中的 NewMap:
1 | result->set_map_after_allocation(*meta_map(), SKIP_WRITE_BARRIER); |
InitializeMap:
1 | map->set_instance_type(type); |
Context
上下文扩展了 FixedArray (src/context.h)。 所以这个 Context 的一个实例是一个 FixedArray,我们可以使用 Get(index)
等来获取数组中的条目。
V8_EXPORT
这可以在 v8 源代码的很多地方找到。 例如:
1 | class V8_EXPORT ArrayBuffer : public Object { |
它是一个预处理器宏,如下所示:
1 |
所以我们可以看到,如果 V8_HAS_ATTRIBUTE_VISIBILITY,并且定义(V8_SHARED),并且如果 BUILDING_V8_SHARED,V8_EXPORT
设置为 __attribute__((可见性(“default”))。但在所有其他情况下,V8_EXPORT
为空并且预处理器不插入任何东西(编译时什么都不会出现。但是 attribute ((visibility(“default”)) 这是什么?
在 GNU 编译器集合 (GCC) 环境中,用于导出的术语是可见性。由于它适用于共享对象中的函数和变量,可见性是指其他共享对象调用 C/
C++ 函数的能力。具有默认可见性的函数具有全局范围,可以从其他共享对象中调用。具有隐藏可见性的函数具有本地范围,不能从其他共享
对象调用。
可见性可以通过使用编译器选项或可见性属性来控制。在您的头文件中,无论您希望在当前动态共享对象 (DSO) 之外公开接口或 API
的任何位置,请将 attribute ((visibility (“default”))) 放在您希望公开的结构、类和函数声明中。使用
-fvisibility=hidden,您是在告诉 GCC,每个未明确标记为可见性属性的声明都具有隐藏的可见性。 build/common.gypi
中有这样一个标志
ToLocalChecked()
您将在 hello_world 示例中看到其中一些调用:
1 | Local<String> source = String::NewFromUtf8(isolate, js, NewStringType::kNormal).ToLocalChecked(); |
NewFromUtf8 实际上返回一个包装在 MaybeLocal 中的 Local ,这会在使用它之前强制检查 Local<> 是否为空。 NewStringType
是一个枚举,可以是 kNormalString(k 表示常量)或 kInternalized。
以下是运行预处理器后(clang -E src/api.cc):
1 | # 5961 "src/api.cc" |
Utils::ToLocal定义
1 | MAKE_TO_LOCAL(ToLocal, String, String) |
以上内容可以在 src/api.h 中找到。 Local
Small Integers
Smi 代表小整数。指针实际上只是一个被视为内存地址的整数。 我们可以使用该内存地址来获取位于该内存插槽中的数据的开始。
但我们也可以只在其中存储一个正常的值,比如 18。
在某些情况下,将一个小整数存储在堆中的某个位置并拥有指向它的指针可能没有意义,而是将值直接存储在指针本身中。
但这仅适用于小整数,因此需要知道我们想要的值是否存储在指针中,或者我们是否应该按照存储到堆中的值来获取值。
64 位机器上的一个字是 8 个字节(64 位),所有的指针都需要对齐到 8 的倍数。所以指针可以是:
1 | 1000 = 8 |
请记住,我们谈论的是指针,而不是存储在它们指向的内存位置的值。 我们可以看到指针中总是有三个位为零。
所以我们可以将它们用于其他用途,并在将它们用作指针时将它们屏蔽掉。
标记涉及借用 32 位中的一位,使其成为 31 位,并让剩余位表示一个标记。 如果标记为零,则这是一个普通值,但如果标记为
1,则必须跟随指针。 这不仅必须用于数字,还可以用于对象(我认为)
相反,小整数由 32 位加上一个指向 64 位数字的指针表示。 V8 需要知道存储在内存中的值是否代表 32 位整数,或者它是否真的是 64
位数字,在这种情况下,它必须跟随指针才能获得完整的值。 这就是标签的概念出现的地方。
Properties/Elements
取以下对象:
1 | { firstname: "Jon", lastname: "Doe' } |
上面的对象有两个命名属性。 命名属性与使用数组时所拥有的整数索引不同。
JavaScript 对象的内存布局:
1 | Properties JavaScript Object Elements |
我们可以看到属性和元素存储在不同的数据结构中。 元素通常实现为普通数组,索引可用于快速访问元素。 但对于属性,情况并非如此。
相反,属性名称和属性索引之间存在映射。
在 src/objects/objects.h 我们可以找到 JSObject:
1 | class JSObject: public JSReceiver { |
并查看 DECL_ACCESSOR 宏:
1 |
|
请注意,JSObject 扩展了 JSReceiver,它被所有可以定义属性的类型扩展。 我认为这包括所有 JSObjects 和 JSProxy。 我们在
JSReceiver 中找到属性数组:
1 | DECL_ACCESSORS(raw_properties_or_hash, Object) |
现在属性(命名属性而不是元素)在内部可以是不同的类型。 这些工作就像外部的简单字典一样,但字典仅在运行时用于某些特定情况。
1 | Properties JSObject HiddenClass (Map) |
JSObject
每个 JSObject 都有一个指向生成的 HiddenClass 的指针作为其第一个字段。 隐藏类包含从属性名称到属性数据类型索引的映射。
当创建一个 JSObject 的实例时,会传入一个 Map。如前所述,JSObject 继承自 JSReceiver,而 JSReceiver 继承自 HeapObject
例如,在 jsobject_test.cc 中,我们首先使用内部 Isolate Factory 创建一个新 Map:
1 | v8::internal::Handle<v8::internal::Map> map = factory->NewMap(v8::internal::JS_OBJECT_TYPE, 24); |
当我们调用 js_object->HasFastProperties() 时,这将委托给map实例:
1 | return !map()->is_dictionary_map(); |
Caching
是在动态语言(例如 JavaScript)中优化多态函数调用的方法。
Lookup caches
向接收者发送消息需要运行时使用接收者的运行时类型找到正确的目标方法。 查找缓存将接收方/
消息名称对的类型映射到方法并存储最近使用的查找结果。 首先查询缓存,如果缓存未命中,则执行正常查找并将结果存储在缓存中。
Inline caches
使用如上所述的查找缓存仍然需要相当长的时间,因为必须为每条消息探测缓存。可以观察到,目标的类型通常没有变化。如果对类型 A
的调用是在特定调用站点完成的,那么下一次调用它时,类型很可能也是 A。系统查找例程查找的方法地址可以被缓存,调用指令可以被覆盖
。后续相同类型的调用可以直接跳转到缓存的方法,完全避免查找。被调用方法的序言必须验证接收者类型没有改变,如果改变了就进行查找
(如果类型不正确,例如不再是 A)。
目标方法地址存储在调用者代码中,或与调用者代码“内联”,因此称为“内联缓存”。
如果 V8 能够对将传递给方法的对象类型做出很好的假设,它可以绕过计算如何访问对象属性的过程,
而是使用之前查找隐藏对象的存储信息类。
Polymorfic Inline cache (PIC)
多态呼叫站点是一个有许多同样可能的接收器类型(因此呼叫目标)的站点。
Monomorfic 表示只有一种接收器类型
Polymorfic 表示几种接收器类型,
Megamorfic 非常多的接收器类型
这种类型的缓存扩展了内联缓存,不仅可以缓存最后一次查找,还可以使用专门生成的存根缓存给定多态调用站点的所有查找结果。假设我们
有一个遍历类型列表并调用方法的方法。如果所有类型都相同(单态),那么 PIC
的作用就像一个内联缓存。调用将直接调用目标方法(方法序言后跟方法体)。如果列表中存在不同的类型,则 prolog 中将出现缓存未命中
并调用查找例程。在正常的内联缓存中,这将重新绑定调用,替换对这种类型目标方法的调用。每次类型更改时都会发生这种情况。
使用 PIC,缓存未命中处理程序将生成一个小的存根例程并将调用重新绑定到该存根。存根将检查接收器是否属于它以前见过的类型并分支到
正确的目标。由于此时目标的类型是已知的,因此它可以直接分支到目标方法体,而无需序言。如果在此之前没有看到该类型,它将被添加到
存根以处理该类型。最终,存根将包含所有使用的类型,并且不会再有缓存未命中/查找。
问题是我们没有类型信息,因此不能直接调用方法,而是查找方法。在静态语言中,可能使用了虚拟表。在 JavaScript
中没有继承关系,因此不可能提前知道 vtable 偏移量。可以做的是观察和了解程序中使用的“类型”。当看到一个对象时,可以存储它,并且
可以存储该方法调用的目标并将其内联到该调用中。基本上,将检查类型,如果在直接调用该方法之前已经看到了该特定类型。但是我们如何
在动态语言中检查类型呢?答案是隐藏类,它允许虚拟机根据隐藏类快速检查对象。
内联缓存源位于 src/ic。
–trace-ic
1 | $ out/x64.debug/d8 --trace-ic --trace-maps class.js |
LoadIC (0->.) 表示它已从未初始化状态 (0) 转换到单态前状态 (.) 单态状态用 1 指定。这些状态可以在 src/ic/ic.cc 中找到。
我们正在做什么缓存有关 StoreIC/LoadIC 调用中先前看到的对象布局的知识。
1 | $ lldb -- out/x64.debug/d8 class.js |
HeapObject
此类描述堆分配的对象。 正是在这个类中,我们找到了有关对象类型的信息。 此信息包含在 v8::internal::Map 中。
v8::internal::Map
src/objects/map.h
bit_field1
bit_field2
bit field3 包含有关此 Map 具有的属性数量的信息,一个指向 DescriptorArray 的指针。 DescriptorArray 包含属性名称和值在
JSObject 中存储的位置等信息。 我注意到这些信息在 src/objects/map.h 中可用。
DescriptorArray
可以在 src/objects/descriptor-array.h 中找到。 此类扩展 FixedArray 并具有以下条目:
1 | [0] the number of descriptors it contains |
Factory
每个内部隔离都有一个用于创建实例的工厂。 这是因为所有句柄都需要使用工厂分配(src/heap/factory.h)
Objects
所有对象都扩展了抽象类 Object (src/objects/objects.h)。
Oddball
此类扩展 HeapObject 并描述空、未定义、真和假对象。
Map
扩展 HeapObject 并且所有堆对象都有一个描述对象结构的 Map。 在这·里您可以找到实例的大小,访问 inobject_properties。
Compiler pipeline
编译脚本时,会解析所有顶层代码。 这些是函数声明(但不是函数体)。
1 | function f1() { <- top level code |
必须预先解析非顶级代码以检查语法错误。 顶层代码由 full-codegen 编译器解析和编译。
该编译器不执行任何优化,唯一的任务是尽快生成机器代码(这是之前的turbofan)
1 | Source ------> Parser --------> Full-codegen ---------> Unoptimized Machine Code |
所以即使我们只为顶层代码生成代码,整个脚本也会被解析。 预解析(语法检查)没有以任何方式存储。 这些函数是惰性存根,
如果函数被调用时,函数就会被编译。 这意味着必须对函数进行解析(同样,第一次是预解析记住)。
如果确定某个函数很热,它将由两个优化编译器之一优化,用于 JavaScript 的旧部分或 Turbofan for Web Assembly (WASM)
和一些较新的 es6 功能。
V8 第一次看到一个函数时会将其解析为 AST,但在使用该函数之前不会对该树进行任何进一步的处理。
1 | +-----> Full-codegen -----> Unoptimized code |
内联缓存 (IC) 在这里完成,这也有助于收集类型信息。 V8 也有一个分析器线程,它监控哪些函数是热的并且应该被优化。
这种分析还允许 V8 找出有关使用 IC 的类型的信息。然后可以将该类型信息馈送到Crankshaft/Turbofan。类型信息存储为 8 位值。
当一个函数被优化时,未优化的代码不能被丢弃,因为它可能是需要的,因为 JavaScript 是高度动态的,优化的函数可能会发生变化,在
这种情况下,我们会退回到未优化的代码。这会占用大量内存,这对于低端设备可能很重要。解析(两次)所花费的时间也需要时间。
Ignition 的想法是成为一个字节码解释器并减少内存消耗,与本机代码相比,字节码非常简洁,本机代码可能因目标平台而异。整个源代码
都可以解析和编译,与当前管道相比,具有上述预解析和解析阶段。所以即使是未使用的函数也会被编译。字节码成为事实的来源,而不是像
之前的 AST。
1 | Source ------> Parser --------> Ignition-codegen ---------> Bytecode ---------> Turbofan ----> Optimized Code ---+ |
Abstract Syntax Tree (AST)
在 src/ast/ast.h 中。 您可以使用 d8 的 –print-ast 选项打印 ast。
让我们使用以下 javascript 并查看 ast:
1 | const msg = 'testing'; |
1 | $ d8 --print-ast simple.js |
可以在 ast.h 中找到 EXPRESSION 的声明。
Bytecode
可以在 src/interpreter/bytecodes.h 中找到
StackCheck 检查堆栈限制是否未超出以防止溢出。
Star 将累加器寄存器中的内容存储在寄存器(操作数)中。
来自寄存器参数 a1 的 Ldar Load 累加器,它是 b
寄存器不是机器寄存器,除了我理解的累加器,而是堆栈分配。
Parsing
解析是对 JavaScript 的解析和抽象语法树的生成。 然后访问该树并从中生成字节码。 本节试图找出这些操作在代码中的何处执行。
例如,以脚本为例。
1 | $ make run-script |
让我们看一下以下行:
1 | Local<Script> script = Script::Compile(context, source).ToLocalChecked(); |
这将使我们进入 api.cc
1 | ScriptCompiler::Source script_source(source); |
CompileUnboundInternal 将调用 GetSharedFunctionInfoForScript(在 src/compiler.cc 中):
1 | result = i::Compiler::GetSharedFunctionInfoForScript( |
LanguageMode 可以在 src/globals.h 中找到,它是一个具有三个值的枚举:
1 | enum LanguageMode : uint32_t { SLOPPY, STRICT, LANGUAGE_END }; |
我认为 SLOPPY 模式是没有“use strict”时的模式;。 请记住,这可以进入函数内部,而不必位于文件的顶层。
1 | ParseInfo parse_info(script); |
有一个单元测试显示如何创建和检查 ParseInfo 实例。
这将调用 ParseInfo 的构造函数(在 src/parsing/parse-info.cc 中),并将调用 ParseInfo::InitFromIsolate:
1 | DCHECK_NOT_NULL(isolate); |
我对这些 ast_string_constants 很好奇:
1 | (lldb) p *ast_string_constants_ |
因此,这些是使用隔离中的值在新 ParseInfo 实例上设置的常量。 不完全确定我想要什么,但我可能稍后会回来。 所以,我们回到 ParseInfo 的构造函数:
1 | set_allow_lazy_parsing(); |
脚本是 v8::internal::Script 类型,可以在 src/object/script.h 中找到
现在回到 compiler.cc 和 GetSharedFunctionInfoForScript 函数:
1 | Zone compile_zone(isolate->allocator(), ZONE_NAME); |
ParseProgram:
1 | Parser parser(info); |
parser.ParseProgram:
1 | Handle<String> source(String::cast(info->script()->source())); |
所以在这里我们可以将我们的 JavaScript 看作一个字符串。
1 | std::unique_ptr<Utf16CharacterStream> stream(ScannerStream::For(source)); |
DoParseProgram:
1 | (lldb) br s -f parser.cc -l 639 |
此调用将落在 parser-base.h 及其 ParseStatementList 函数中。
1 | (lldb) br s -f parser-base.h -l 4695 |
这将出现在 CompileTopelevel 中(在同一个文件 src/compiler.cc 中):
1 | // Compile the code. |
这将出现在 CompileUnoptimizedCode 中(在同一文件中,即 src/compiler.cc):
1 | // 准备并执行最外层函数的编译。 |
PrepareJobImpl:
1 | CodeGenerator::MakeCodePrologue(parse_info(), compilation_info(), |
codegen.cc MakeCodePrologue:
interpreter.cc ExecuteJobImpl:
1 | generator()->GenerateBytecode(stack_limit()); |
src/interpreter/bytecode-generator.cc
1 | RegisterAllocationScope register_scope(this); |
字节码是基于寄存器的(如果这是正确的术语),我们之前有一个例子。 我猜这就是这个call的目的。
VisitDeclarations 将遍历文件中的所有声明,在我们的例子中是:
1 | var user1 = new Person('Fletch'); |
因此该调用将输出一个堆栈检查指令,如上例所示:
1 | 14 E> 0x2eef8d9b103e @ 0 : 7f StackCheck |
Performance
假设您有完整代码生成编译器可能产生的表达式 x + y:
1 | movq rax, x |
如果 x 和 y 是整数,则使用加法运算会快得多:
1 | movq rax, x |
回想一下,函数是经过优化的,所以如果编译器必须退出并取消优化函数的一部分,那么整个函数都会受到影响,它会回到未优化的版本。
Bytecode
本节将研究以下 JavaScript 的字节码:
1 | function beve() { |
首先是没有名称的main:
1 | [generating bytecode for function: ] |
LdaConstant 将常量池中的索引处的常量加载到累加器中。
Star 将累加器寄存器的内容存储在 dst 中。
Ldar 使用来自寄存器 src 的值加载累加器。
LdaGlobal 使用 typeof 外的 FeedBackVector 槽将常量池条目 idx 中的全局名称加载到累加器中。
mov , 存放寄存器的值
这些指令的声明在 src/interpreter/interpreter-generator.cc。
Unified code generation architecture
FeedbackVector
附属于每一个功能,负责记录和管理所有的执行反馈,这是关于类型启用的信息。 您可以在 src/feedback-vector.h 中找到此类的声明
BytecodeGenerator
目前是 V8 中唯一关于 AST 的部分。
BytecodeGraphBuilder
基于解释器字节码生成高级 IR 图。
TurboFan
是一个编译器后端,它接收控制流图,然后进行指令选择、寄存器分配和代码生成。 代码生成生成
Execution/Runtime
我不确定 V8 是否完全遵循这一点,但我听说并读到当引擎遇到函数声明时,它只会解析和验证语法并将 ref 保存到函数名。
在这个阶段不检查函数内的语句,只检查函数声明的语法(括号、参数、括号等)。
Function methods
Function 的声明可以在 include/v8.h 中找到(只是注意到这一点,因为我已经找了好几次了)
Symbol
Symbol 类的声明可以在 v8.h 中找到,内部实现可以在 src/api/api.cc 中找到。
众所周知的符号是使用宏生成的,因此您可以使用诸如“GetToPrimitive”之类的静态函数名称进行搜索来找到它。
1 |
|
所以 GetToPrimitive 会变成:
1 | Local<Symbol> v8::Symbol::GeToPrimitive(Isolate* isolate) { |
Builtins
是 V8 提供的 JavaScript 函数/对象。 这些是使用 C++ DSL 构建的,并通过:
1 | CodeStubAssembler -> CodeAssembler -> RawMachineAssembler. |
内置程序需要为它们生成字节码,以便它们可以在 TurboFan 中运行。
src/code-stub-assembler.h
所有内置函数都由 BUILTIN_LIST_BASE 宏在 src/builtins/builtins-definitions.h 中声明。 有不同类型的内置函数(TF = Turbo Fan):
TFJ JavaScript 链接,这意味着它可以作为 JavaScript 函数调用
TFS CodeStub 链接。 带有存根链接的内置函数可用于将公共代码提取到单独的代码对象中,然后多个调用者可以使用该代码对象。
这些很有用,因为内置函数在编译时生成并包含在 V8 快照中。 这意味着它们是创建的每个隔离的一部分。 能够为多个内置函数共享通用代码将节省空间。
TFC CodeStub 与自定义描述符的链接
要了解它是如何工作的,我们首先需要禁用快照。 如果不这样做,我们将无法设置断点,因为堆将在编译时序列化并在 v8 启动时反序列化。
要找到禁用快照的选项,请使用:
1 | $ gn args --list out.gn/learning --short | more |
构建完成后,我们应该能够在 bootstrapper.cc 及其函数 Genesis::InitializeGlobal 中设置断点:
1 | (lldb) br s -f bootstrapper.cc -l 2684 |
让我们看看 JSON 对象是如何设置的:
1 | Handle<String> name = factory->InternalizeUtf8String("JSON"); |
TENURED 意味着这个对象应该直接在old generation分配。
1 | JSObject::AddProperty(global, name, json_object, DONT_ENUM); |
DONT_ENUM 由一些内置函数检查,如果设置此对象将被这些函数忽略。
1 | SimpleInstallFunction(json_object, "parse", Builtins::kJsonParse, 2, false); |
在这里我们可以看到我们正在安装一个名为 parse 的函数,它有 2 个参数。 定义在 src/builtins/builtins-json.cc。
SimpleInstallFunction 有什么作用?
让我们以控制台为例,它是使用以下方法创建的:
1 | Handle<JSObject> console = factory->NewJSObject(cons, TENURED); |
所以我们可以看到 base 是我们对 JSObject 的句柄,名称是“debug”。 Builtins::Name 是 Builtins:kConsoleDebug。 这是在哪里定义的?
你可以在 src/builtins/builtins-definitions.h 中找到一个名为 CPP 的宏:
CPP(控制台调试)
这个宏扩展成什么?
它是 builtin-definitions.h 中 BUILTIN_LIST_BASE 宏的一部分 我们必须查看 BUILTIN_LIST 的使用位置,我们可以在 builtins.cc 中找到它。
在builtins.cc 中,我们有一个BuiltinMetadata 数组,声明为:
1 | const BuiltinMetadata builtin_metadata[] = { |
这将扩展到在数组中创建 BuiltinMetadata 结构条目。 BuildintMetadata 结构看起来像这样,这可能有助于理解发生了什么:
1 | struct BuiltinMetadata { |
因此 CPP(ConsoleDebug) 将扩展为数组中的一个条目,如下所示:
1 | { ConsoleDebug, |
第三个参数是联合上的创建,这可能并不明显。
回到我试图回答的问题是:
“Buildtins::Name 是 Builtins:kConsoleDebug。这是在哪里定义的?”
为此,我们必须查看 builtins.h 和枚举名称:
1 | enum Name : int32_t { |
这将使用 DEF_ENUM 宏扩展为 builtin-definitions.h 中内置函数的完整列表。 所以 ConsoleDebug 的扩展看起来像:
1 | enum Name: int32_t { |
因此,备份查看 SimpleInstallFunction 的参数,它们是:
1 | SimpleInstallFunction(console, "debug", Builtins::kConsoleDebug, 1, false, |
我们知道 Builtins::Name,所以让我们看看 len 是一个,这是什么?
SimpleInstallFunction 将调用:
1 | Handle<JSFunction> fun = |
如果 adapt 为真,则使用 len,但在我们的例子中为假。 如果 adapt 为真,这就是它的用途:
1 | fun->shared()->set_internal_formal_parameter_count(len); |
我不确定这里指的是什么适应。
未指定 PropertyAttributes,因此它将获得 DONT_ENUM 的默认值。 最后一个类型为 BuiltinFunctionId 的参数也未指定,因此将使用
kInvalidBuiltinFunctionId 的默认值。 这是在 src/objects/objects.h 中定义的枚举。
此博客(https://v8project.blogspot.se/2017/11/csa.html) 提供了一个向 String 对象添加函数的示例。
1 | $ out.gn/learning/mksnapshot --print-code > output |
然后,您可以从中查看生成的代码。 这将生成一个可以通过 C++ 调用的代码存根。 让我们更新它以从 JavaScript 调用它:
更新 builtins/builtins-string-get.cc :
1 | TF_BUILTIN(GetStringLength, StringBuiltinsAssembler) { |
我们还必须更新 builtins/builtins-definitions.h:
1 | TFJ(GetStringLength, 0) |
和 bootstrapper.cc:
1 | SimpleInstallFunction(prototype, "len", Builtins::kGetStringLength, 0, true); |
如果您现在使用 ‘ninja -C out.gn/learning_v8’ 构建,您应该能够运行 d8 并尝试一下:
1 | d8> const s = 'testing' |
现在让我们仔细看看为此生成的代码:
1 | $ out.gn/learning/mksnapshot --print-code > output |
查看生成的输出,我惊讶地看到 GetStringLength 的两个条目(我更改名称只是为了确保没有其他东西生成第二个条目)。 为什么是两个?
以下使用英特尔汇编语法,这意味着没有寄存器/立即数前缀,第一个操作数是目标,第二个操作数是源。
1 | --- Code --- |
TF_BUILTIN macro
是定义 Turbofan (TF) 内置函数的宏,可以在 builtins/builtins-utils-gen.h 中找到
如果我们看一下文件 src/builtins/builtins-bigint-gen.cc 和以下函数:
1 | TF_BUILTIN(BigIntToI64, CodeStubAssembler) { |
让我们以上面的 GetStringLength 示例为例,看看处理此宏后将扩展为什么:
1 | $ clang++ --sysroot=build/linux/debian_sid_amd64-sysroot -isystem=./buildtools/third_party/libc++/trunk/include -isystem=buildtools/third_party/libc++/trunk/include -I. -E src/builtins/builtins-bigint-gen.cc > builtins-bigint-gen.cc.pp |
1 | static void Generate_BigIntToI64(compiler::CodeAssemblerState* state); |
从生成的类中,您可以看到如何在 TF_BUILTIN 宏中使用 Parameter。
Building V8
您需要将 Google V8 源代码签出到您的本地文件系统,并按照此处的说明进行构建。()
为学习-v8配置v8
有一个 make 目标可以为 V8 生成特定于该项目的构建配置。 可以使用以下命令运行它:
1 | $ make configure_v8 |
然后编译该项目
1 | $ make compile_v8 |
gclient sync
1 | gclient sync #安装依赖 |
Troubleshooting build(构建疑难解答):
1 | /v8_src/v8/out/x64.release/obj/libv8_monolith.a(eh-frame.o):eh-frame.cc:function v8::internal::EhFrameWriter::WriteEmptyEhFrame(std::__1::basic_ostream<char, std::__1::char_traits<char> >&): error: undefined reference to 'std::__1::basic_ostream<char, std::__1::char_traits<char> >::write(char const*, long)' |
-stdlib=libc++ 是 llvm 的 C++ 运行模式.此运行时具有 __1 命名空间。
看起来上面的静态库是用 clangs/llvm 的 libc++ 编译的,因为我们看到了 __1 命名空间。
-stdlib=libstdc++ 是 GNU 的 C++ 运行模式
命名空间 std::__1 被使用,因此是 libc++ 的命名空间,它是 clangs libc++ 库。
解决问题方式
1.编译时更改 v8 构建以使用 glibc++,以便链接它时符号正确
2.更新链接器 (ld) 以使用 libc++。
在链接期间包含要链接的正确库,这需要指定:
1 | -stdlib=libc++ -Wl,-L$(v8_build_dir) |
如果我们查看 $(v8_build_dir),我们会找到 libc++.so。 我们还需要动态链接器在运行时使用 LD_LIBRARY_PATH 找到这个库:
1 | $ LD_LIBRARY_PATH=../v8_src/v8/out/x64.release/ ./hello-world |
这是使用我们路径中的 ld 。 我们可以通过 -B 选项告诉 clang 使用不同的搜索路径:
1 | $ clang++ --help | grep -- '-B' |
libgcc_s 是 GCC 低级运行时库。 和 glibc++ 库不同。
运行 cctest:
1 | $ out.gn/learning/cctest test-heap-profiler/HeapSnapshotRetainedObjectInfo |
要获取可用测试的列表:
1 | $ out.gn/learning/cctest --list |
检查格式化/linting:
1 | git cl format |
然后您可以 git diff 并查看更改。
运行提交前检查:
1 | $ git cl presubmit |
然后使用上传:
1 | $ git cl upload |
Build details
所以当我们运行 gn 时,它会生成 Ninja 构建文件。 GN 本身是用 C++ 编写的,但有一个 Python 包装器。
gn 中的组只是其他目标的集合,使它们能够具有名称。
所以当我们运行 gn 时会生成一些 .ninja 文件。 如果我们查看输出目录的根目录,我们会发现两个 .ninja 文件:
1 | build.ninja toolchain.ninja |
默认情况下,ninja 会查找 build.ninja,当我们运行 ninja 时,我们通常会指定 -C out/dir。
如果没有在命令行上指定目标,ninja将执行所有输出,除非有一个指定为默认值。 V8 具有以下默认目标:
1 | default all |
虚假规则可用于为其他目标创建别名。 ninja 中的 $ 是一个转义字符,因此对于 all 目标,它会转义新行,就像在 shell 脚本中使用 \ 一样。
让我们看一下 bytecode_builtins_list_generator:
1 | build $:bytecode_builtins_list_generator: phony ./bytecode_builtins_list_generator |
ninja build 语句的格式为:
1 | build outputs: rulename inputs |
我们再次看到 $ ninja 转义字符,但这次它转义了冒号,否则会被解释为分隔文件名。 这种情况下的输出是 bytecode_builtins_list_generator。
我猜,因为我找不到 ./bytecode_builtins_list_generator 和
在这种情况下,默认的 target_out_dir 是 //out/x64.release_gcc/obj。 生成此文件的 BUILD.gn
中的可执行文件未指定任何输出目录,因此我假设生成的 .ninja 文件位于 target_out_dir 中,在这种情况下我们可以找到 bytecode_builtins_list
_generator.ninja 该文件有一个名为:
1 | label_name = bytecode_builtins_list_generator |
嗯,请注意在 build.ninja 中有以下命令:
1 | subninja toolchain.ninja |
在 toolchain.ninja 我们有:
1 | subninja obj/bytecode_builtins_list_generator.ninja |
这就是使 ./bytecode_builtins_list_generator 可用的原因。
1 | $ ninja -C out/x64.release_gcc/ -t targets all | grep bytecode_builtins_list_generator |
好的,所以我想了解在过程中何时运行torque以生成 TorqueGeneratedStruct 之类的类:
1 | class Struct : public TorqueGeneratedStruct<Struct, HeapObject> { |
1 | ./torque $ |
和之前一样,我们可以在 toolchain.ninja 的 subninja 命令中找到 obj/torque.ninja:
1 | subninja obj/torque.ninja |
这是建立可执行torque,但它还没有运行。
1 | $ gn ls out/x64.release_gcc/ --type=action |
注意 run_torque 目标
1 | $ gn desc out/x64.release_gcc/ //:run_torque |
如果我们查看 toolchain.ninja,我们有一个名为 ___run_torque___build_toolchain_linux_x64__rule 的规则
1 | command = python ../../tools/run.py ./torque -o gen/torque-generated -v8-root ../.. |
并且有一个构建指定 gen/torque-generated 中的 .h 和 cc 文件,如果它们发生更改,则其中包含此规则。
Building chromium
在对 V8 进行更改时,您可能需要验证您的更改没有破坏 Chromium 中的任何内容。
生成您的项目(gpy):您必须在构建之前运行一次:
1 | $ gclient sync |
Update the code base
1 | $ git fetch origin master |
Building using GN
1 | $ gn args out.gn/learning |
Building using Ninja
1 | Building using Ninja |
构建测试:
1 | $ ninja -C out.gn/learning chrome/test:unit_tests |
我第一次构建时遇到的错误:
1 | traceback (most recent call last): |
我能够通过以下方式解决这个问题:
1 | $ pip install -U pyobjc |
Using a specific version of V8
以下说明有效,但也可以创建从 chromium/src/v8 到本地 v8 存储库和构建/测试的软链接。
因此,我们希望包含我们的 V8 更新版本,以便我们可以通过对 V8 的更改来验证它是否正确构建。 虽然我不确定这是不是正确的方法,但我能够更新 src (
chromium) 中的 DEPS 并将 v8 条目设置为 git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87:
1 | "git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87" |
在此之后,您必须运行 gclient sync。
另一种方法是不更新 DEPS 文件,这是一个版本控制文件,而是更新 .gclientrc 并添加一个 custom_deps 条目:
1 | solutions = [{u'managed': False, u'name': u'src', u'url': u'https://chromium.googlesource.com/chromium/src.git', |
Buiding pdfium
您可能必须编译这个项目(除了 chromium 来验证 v8 中的更改不会破坏 pdfium.xml 中的代码)。
Create/clone the project
1 | $ mkdir pdfuim_reop |
Building
1 | $ ninja -C out/Default |
Using a branch of v8
您应该能够更新 .gclient 文件,添加一个 custom_deps 条目:
1 | solutions = [ |
] cache_dir = None 在此之后您也必须运行 gclient sync。
Code in this repo
hello-world(https://github.com/danbev/learning-v8/blob/master/hello-world.cc)
hello-world 被大量评论并展示了从 JavaScript 公开和访问静态 int 的用法。
instances(https://github.com/danbev/learning-v8/blob/master/instances.cc)
实例展示了从 JavaScript 创建 C++ 类的新实例的用法。
run-script(https://github.com/danbev/learning-v8/blob/master/run-script.cc)
run-script 与 instance 基本相同,但读取外部文件 script.js 并运行脚本。
tests
测试目录包含 V8 中各个类/概念的单元测试,以帮助理解它们。
Building this projects code
1 | $ make |
Running
1 | $ ./hello-world |
Cleaning
1 | $ make clean |
Contributing a change to V8
1.使用 git new-branch name 创建一个工作分支
2.git cl 上传
有关更多详细信息,请参阅 Google 的贡献代码。(https://www.chromium.org/developers/contributing-code)
Find the current issue number
1 | $ git cl issue |
Debugging
1 | $ lldb hello-world |
src/objects-printer.cc 中有许多有用的函数,它们也可以在 lldb 中使用。
Print value of a Local object
1 | (lldb) print _v8_internal_Print_Object(*(v8::internal::Object**)(*init_fn)) |
Print stacktrace
1 | (lldb) p _v8_internal_Print_StackTrace() |
Creating command aliases in lldb
创建一个名为 .lldbinit 的文件(在您的项目目录或主目录中)。 这个文件现在可以在 v8 的工具目录中找到。
Using d8
这是用于以下示例的源文件:
1 | $ cat class.js |
V8_shell startup
运行 v8_shell 时会发生什么?
1 | $ lldb -- out/x64.debug/d8 --enable-inspector class.js |
首先调用 v8::base::debug::EnableInProcessStackDumping(),然后调用一些由宏保护的特定于 Windows 的代码。 接下来是使用 v8::Shell::SetOptions
设置所有选项
SetOptions 将调用 src/api.cc 中的 v8::V8::SetFlagsFromCommandLine:
1 | i::FlagList::SetFlagsFromCommandLine(argc, argv, remove_flags); |
这个函数可以在 src/flags.cc 中找到。 标志本身在 src/flag-definitions.h 中定义
接下来创建一个新的 SourceGroup 数组:
1 | options.isolate_sources = new SourceGroup[options.num_isolates]; |
然后执行检查以查看 args 是否为 –isolate 或 –module,或 -e,如果不是(如我们的例子中)
1 | } else if (strncmp(str, "-", 1) != 0) { |
TODO:我不完全确定 SourceGroups 是关于什么的,但只是注意到这一点,稍后会重新讨论。
这将带我们回到 src/d8.cc 中的 int Shell::Main
1 | ::V8::InitializeICUDefaultLocation(argv[0], options.icu_data_file); |
更多细节请参见 ICU。
接下来初始化默认的 V8 平台:
1 | g_platform = i::FLAG_verify_predictable ? new PredictablePlatform() : v8::platform::CreateDefaultPlatform(); |
v8::platform::CreateDefaultPlatform() 在我们的例子中会被调用。
然后我们回到 Main 并有以下几行:
1 | 2685 v8::V8::InitializePlatform(g_platform); |
这与我在 Node.js 启动过程中看到的非常相似。(https://github.com/danbev/learning-nodejs#startint-argc-char-argv)
我们没有在命令行上指定任何 natives_blob 或 snapshot_blob 作为选项,因此将使用默认值:
1 | v8::V8::InitializeExternalStartupData(argv[0]); |
回到 src/d8.cc 第 2918 行:
1 | Isolate* isolate = Isolate::New(create_params); |
此调用将带我们进入 api.cc 第 8185 行:
1 | i::Isolate* isolate = new i::Isolate(false); |
因此,我们正在调用 Isolate 构造函数(在 src/isolate.cc 中)。
1 | isolate->set_snapshot_blob(i::Snapshot::DefaultSnapshotBlob()); |
api.cc:
1 | isolate->Init(NULL); |
src/builtins/builtins.cc,这是定义内置函数的地方。 TODO:理清这些宏的作用。
在 src/v8.cc 中,我们检查了传递的选项是否用于压力运行,但由于我们没有传递任何此类标志,因此将遵循此代码路径,该路径将调用 RunMain:
1 | result = RunMain(isolate, argc, argv, last_run); |
这将最终调用:
1 | options.isolate_sources[0].Execute(isolate); |
这将调用 SourceGroup::Execute(Isolate* isolate)
1 | // Use all other arguments as names of files to load and run. |
它将委托给 ScriptCompiler(Local, Source* source, CompileOptions options):
1 | auto maybe = CompileUnboundInternal(isolate, source, options); |
CompileUnboundInternal
1 | result = i::Compiler::GetSharedFunctionInfoForScript( |
src/compiler.cc
1 | // Compile the function and add it to the cache. |
回到 src/compiler.cc-info.cc:
1 | result = CompileToplevel(&info); |
回到d8.cc:
1 | maybe_result = script->Run(realm); |
src/api.cc
1 | auto fun = i::Handle<i::JSFunction>::cast(Utils::OpenHandle(this)); |
1 | i::Handle<i::Object> receiver = isolate->global_proxy(); |
src/execution.cc
Zone
直接取自 src/zone/zone.h:
1 | // Zone支持非常快速地分配小块 |
V8 flags
1 | $ ./d8 --help |
d8
1 | (lldb) br s -f d8.cc -l 2935 |
v8::String::NewFromOneByte
所以第一次看到这个函数名的时候有点迷茫,还以为跟字符串的长度有关系。 但是字节是构成字符串的字符的类型。 例如,一个单字节字符将被重新解释为
uint8_t:
1 | const char* data |
task:
gdbinit 已更新。 检查是否有应该移植到 lldbinit 的东西
Invocation walkthrough
本节将通过调用脚本来了解 V8 中发生了什么。
我将使用 run-scripts.cc 作为示例。
1 | $ lldb -- ./run-scripts |
我将逐步完成,直到以下调用:
1 | script->Run(context).ToLocalChecked(); |
所以,Script::Run 是在 api.cc 中定义的。首先在这个函数中发生的事情是一个宏:
1 | PREPARE_FOR_EXECUTION_WITH_CONTEXT_IN_RUNTIME_CALL_STATS_SCOPE( |
那么,预处理器用什么来代替它:
1 | auto isolate = context.IsEmpty() ? i::Isolate::Current() : reinterpret_cast<i::Isolate*>(context->GetIsolate()); |
我现在跳过 TRACE_EVENT_CALL_STATS_SCOPED。 PREPARE_FOR_EXECUTION_GENERIC 将被替换为:
1 | if (IsExecutionTerminatingCheck(isolate)) { \ |
i::JSFunction 的代码在 src/api.h 中生成。 让我们仔细看看这个。
1 | #define DECLARE_OPEN_HANDLE(From, To) \ |
OPEN_HANDLE_LIST 如下:
1 | #define OPEN_HANDLE_LIST(V) \ |
所以让我们为 JSFunction 扩展它,它应该变成:
1 | static inline v8::internal::Handle<v8::internal::JSFunction> \ |
因此将有一个名为 OpenHandle 的函数,它将接受一个指向 Script 的 const 指针。
在 src/api.h 再往下一点,还有另一个宏,如下所示:
1 | OPEN_HANDLE_LIST(MAKE_OPEN_HANDLE) |
MAKE_OPEN_HANDLE:
1 | #define MAKE_OPEN_HANDLE(From, To) |
请记住,JSFunction 包含在 OPEN_HANDLE_LIST 中,因此在预处理器处理此标头后,源代码中将包含以下内容:具体示例如下所示:
1 | v8::internal::Handle<v8::internal::JSFunction> Utils::OpenHandle( |
您可以使用以下命令检查预处理器的输出:
$ clang++ -I./out/x64.release/gen -I. -I./include -E src/api/api-inl.h > api-inl.output
那么 JSFunction 是在哪里声明的呢? 它在objects.h中定义
Ignition interpreter
用户 JavaScript 还需要为他们生成字节码,他们还使用 C++ DLS 并使用 CodeStubAssembler -> CodeAssembler ->
RawMachineAssembler,就像内置函数一样。
C++ Domain Specific Language (DLS)
Build failure
rebase后,我看到了以下问题:
1 | $ ninja -C out/Debug chrome |
“解决方案”是删除 out 目录并重建。
Tasks
要找到合适的任务,您可以在 bugs.chromium.org 上使用 label:HelpWanted。
OpenHandle
这个调用有什么作用:
1 | Utils::OpenHandle(*(source->source_string)); |
这是 src/api.h 中定义的宏:
1 | #define MAKE_OPEN_HANDLE(From, To) \ |
如果我们仔细看一下宏,在我们的例子中应该扩展成这样的:
1 | v8::internal::Handle<v8::internal::To> Utils::OpenHandle(const v8:String* that, false) { |
所以这会返回一个新的 v8::internal::Handle,构造函数在 src/handles.h:95 中定义。
src/objects.cc Handle WeakFixedArray::Add(Handle maybe_array, 10167 Handle value, 10168 int* assigned_index) { 注意第一个参数的名称
maybe_array 但它不是maybe类型?
Context
JavaScript 提供了一组内置函数和对象。 这些功能和对象可以通过用户代码进行更改。 每个上下文都是这些对象和函数的单独集合。
并且 internal::Context 在 deps/v8/src/contexts.h 中声明并扩展 FixedArray。
1 | class Context: public FixedArray { |
可以通过调用创建上下文:
1 | const v8::HandleScope handle_scope(isolate_); |
Context::New 可以在 src/api.cc:6405 中找到:
1 | Local<Context> v8::Context::New( |
该函数的声明可以在 include/v8.h 中找到:
1 | static Local<Context> New( |
所以我们可以看到我们不必指定 internal_fields_deserialize 的原因。 什么是扩展配置?
这个类可以在 include/v8.h 中找到,它只有两个成员,一个扩展名的计数和一个包含名称的数组。
如果指定,这些将由 Boostrapper::InstallExtensions 安装,后者将委托给 Genesis::InstallExtensions,两者都可以在 src/boostrapper.cc 中找到。
扩展在哪里注册?
每个进程执行一次,并从 V8::Initialize() 调用:
1 | void Bootstrapper::InitializeOncePerProcess() { |
扩展可以在 src/extensions 中找到。 您注册自己的扩展,可以在 test/context_test.cc 中找到一个示例。
1 | (lldb) br s -f node.cc -l 4439 |
这个输出被拿走了
创建一个新的上下文是由 v8::CreateEnvironment 完成的
1 | (lldb) br s -f api.cc -l 6565 |
1 | InvokeBootstrapper<ObjectType> invoke; |
这稍后将在 Snapshot::NewContextFromSnapshot 中结束:
1 | Vector<const byte> context_data = |
所以我们可以在这里看到 Context 是从快照中反序列化的。 在这个阶段上下文包含什么:
1 | (lldb) expr result->length() |
让我们看一个条目:
1 | (lldb) expr result->get(0)->Print() |
所以我们可以看到这是 [Function] 类型,我们可以使用:
1 | (lldb) expr JSFunction::cast(result->get(0))->code()->Print() |
1 | (lldb) expr JSFunction::cast(result->closure())->Print() |
所以这是与反序列化上下文关联的 JSFunction。 不知道这是什么,因为查看源代码它看起来像一个空函数。
也可以在上下文中设置一个函数,所以我猜这可以访问设置后的上下文函数。 函数集在哪里,它可能是反序列化的,但我们可以看到它在 deps/v8/src/
bootstrapper.cc 中使用:
1 | { |
Context::Scope 是一个用于进入/退出上下文的 RAII 类。 让我们仔细看看 Enter:
1 | void Context::Enter() { |
所以当前上下文被保存,然后这个上下文环境被设置为隔离上的当前。 EnterContext 将推送传入的上下文(deps/v8/src/api.cc):
1 | void HandleScopeImplementer::EnterContext(Handle<Context> context) { |
1 | DetachableVector is a delegate/adaptor with some additonaly features on a std::vector. |
现在,SaveContext 正在使用当前上下文,而不是此上下文 (env),并将其推送到 saved_contexts_vector的末尾。 当我们从 context_scope1 进入 context_
scope2 时,我们可以看到这个:
退出看起来像:
1 | void Context::Exit() { |
EmbedderData
上下文可以在其上设置embedder data。 就像上面描述的一样,Context 内部是一个 FixedArray。 Context中的SetEmbedderData在src/api.cc中实现:
1 | const char* location = "v8::Context::SetEmbedderData()"; |
location 仅用于记录,我们现在可以忽略它。 EmbedderDataFor:
1 | i::Handle<i::Context> env = Utils::OpenHandle(context); |
我们可以在 src/contexts-inl.h 中找到 embedder_data
1 | #define NATIVE_CONTEXT_FIELD_ACCESSORS(index, type, name) \ |
以及 context.h 中的 NATIVE_CONTEXT_FIELDS:
1 | #define NATIVE_CONTEXT_FIELDS(V) \ |
因此预处理器会将其扩展为:
1 | FixedArray embedder_data() const; |
我们可以看一下初始数据:
1 | lldb) expr data->Print() |
设置后:
1 | (lldb) expr data->Print() |
ENTER_V8_FOR_NEW_CONTEXT
此宏用于 CreateEnvironment (src/api.cc),此函数中的调用如下所示:
1 | ENTER_V8_FOR_NEW_CONTEXT(isolate); |
Factory::NewMap
本节将看看以下调用:
1 | i::Handle<i::Map> map = factory->NewMap(i::JS_OBJECT_TYPE, 24); |
让我们仔细看看可以在 src/factory.cc 中找到的这个函数:
1 | Handle<Map> Factory::NewMap(InstanceType type, int instance_size, |
如果我们查看 factory.h,我们可以看到 elements_kind 和 inobject_properties 的默认值:
1 | Handle<Map> NewMap(InstanceType type, int instance_size, |
如果我们展开 CALL_HEAP_FUNCTION 宏,我们将得到:
1 | AllocationResult __allocation__ = isolate()->heap()->AllocateMap(type, |
所以,让我们看一下’src/heap/heap.cc’中的isolate()->heap()->AllocateMap:
1 | HeapObject* result = nullptr; |
AllocateRaw 可以在 src/heap/heap-inl.h 中找到:
1 | bool large_object = size_in_bytes > kMaxRegularHeapObjectSize; |
1 | (lldb) expr large_object |
AllocateRawUnaligned 可以在 src/heap/spaces-inl.h 中找到
1 | HeapObject* object = AllocateLinearly(size_in_bytes); |
v8::internal::Object
是对象层次结构中所有类的抽象超类,Smi 和 HeapObject 都是 Object 的子类,因此仅对象函数中没有数据成员。例如:
1 | bool IsObject() const { return true; } |
v8::internal::Smi
扩展 v8::internal::Object 并且不在堆上分配。 没有成员,因为指针本身用于存储信息。
在我们的例子中,调用 v8::Isolate::New 由测试夹具完成:
1 | virtual void SetUp() { |
这将调用:
1 | Isolate* Isolate::New(const Isolate::CreateParams& params) { |
在 Isolate::Initialize 中,我们将调用 i::Snapshot::Initialize(i_isolate):
1 | if (params.entry_hook || !i::Snapshot::Initialize(i_isolate)) { |
这将调用:
1 | bool success = isolate->Init(&deserializer); |
在此调用之前,所有根都未初始化。 阅读这个博客它说 Isolate 类包含一个根表。 在我看来,堆包含这个数据结构,但也许这就是他们的意思。
1 | (lldb) bt 3 |
在 startup-deserializer.cc 我们可以找到 StartupDeserializer::DeserializeInto:
1 | DisallowHeapAllocation no_gc; |
如果我们查看 src/roots.h 之后,我们可以在堆中找到只读根。 如果我们取 10 的值,即:
1 | V(String, empty_string, empty_string) \ |
然后我们可以检查这个值:
1 | (lldb) expr roots_[9] |
因此,此条目是指向托管堆上已从快照反序列化的对象的指针。
堆类有很多成员,在构造过程中由构造函数的主体初始化,如下所示:
1 | { |
我们可以看到,roots_ 填充了 0 个值。 我们可以使用以下方法检查roots_:
1 | (lldb) expr roots_ |
现在在这个阶段它们都是 0,那么这个数组什么时候会被填充?
这些将在 Isolate::Init 中发生:
1 | heap_.SetUp() |
这将委托给调用 Heap::ConfigureHeap 的 ConfigureHeapDefaults():
1 | enum RootListIndex { |
1 | (lldb) expr heap->RootListIndex::kFreeSpaceMapRootIndex |