这是我的理解文档 https://github.com/v8/v8/wiki/Embedder%27s-Guide#handles-and-garbage-collection和handles-inl.h https://github.com/v8/v8/blob/dab61bc310921d70e4d02275f7a09fa352466370/src/handles-inl.h源代码。我也可能是完全错误的,因为我不是 V8 开发人员,而且文档也很少。
垃圾收集器有时会将内容从一个内存位置移动到另一个内存位置,并且在一次此类扫描期间,还会检查哪些对象仍然可访问,哪些对象不可访问。与引用计数类型相比,例如std::shared_ptr
,这能够检测和收集循环数据结构。为了使所有这一切发挥作用,V8 必须清楚哪些对象是可访问的。
另一方面,在某些计算的内部,对象被大量创建和删除。您不希望每个此类操作有太多开销。实现此目的的方法是创建句柄堆栈。该堆栈中列出的每个对象都可以从某些 C++ 计算中的某个句柄获得。除此之外,还有持久句柄,这可能需要更多的工作来设置,并且可以在 C++ 计算之外继续存在。
拥有一个引用堆栈需要您以类似堆栈的方式使用它。该堆栈中没有“无效”标记。堆栈中从底部到顶部的所有对象都是有效的对象引用。确保这一点的方法是LocalScope
。它使事物保持分层。使用引用计数指针,您可以执行以下操作:
shared_ptr<Object>* f() {
shared_ptr<Object> a(new Object(1));
shared_ptr<Object>* b = new shared_ptr<Object>(new Object(2));
return b;
}
void g() {
shared_ptr<Object> c = *f();
}
这里首先创建了对象 1,then创建对象 2,然后函数返回,对象 1 被销毁,然后对象 2 被销毁。这里的关键点是,存在一个时间点,对象 1 无效,但对象 2 仍然有效。就是这样LocalScope
旨在避免。
其他一些 GC 实现会检查 C 堆栈并查找在那里找到的指针。这很有可能出现误报,因为实际上是数据的东西可能会被误解为指针。对于可访问性来说,这似乎相当无害,但是当由于移动对象而重写指针时,这可能是致命的。它还有许多其他缺点,并且很大程度上依赖于该语言的低级实现的实际工作方式。 V8 通过将句柄堆栈与函数调用堆栈分开来避免这种情况,同时确保它们充分对齐以保证提到的层次结构要求。
提供另一种比较:一个对象仅被一个对象引用shared_ptr
一旦其 C++ 块作用域结束,就变得可收集(并且实际上将被收集)。由 a 引用的对象v8::Handle
当离开最近包含的封闭范围时将变得可收集HandleScope
目的。因此程序员可以更好地控制堆栈操作的粒度。在性能很重要的紧密循环中,仅维护一个单一的可能会很有用HandleScope
对于整个计算,这样您就不必经常访问句柄堆栈数据结构。另一方面,这样做将在整个计算期间保留所有对象,如果这是一个迭代许多值的循环,这确实会非常糟糕,因为所有对象都会保留到最后。但程序员拥有完全的控制权,可以以最合适的方式安排事情。
就我个人而言,我会确保构建一个HandleScope
- 在可能从代码外部调用的每个函数的开头。这可以确保您的代码会自行清理。
- 在每个循环的主体中,可能会看到超过三个左右的迭代,因此您只保留当前迭代中的变量。
- 围绕每个代码块,后面跟着一些回调调用,因为这可以确保如果回调需要更多内存,您的东西可以得到清理。
- 每当我觉得某些东西可能会产生大量中间数据时,这些数据应该尽快清理(或至少变得可收集)。
一般来说我不会创建HandleScope
对于每个内部函数,如果我可以确定调用此函数的每个其他函数都已经设置了一个HandleScope
。但这可能是一个品味问题。