内存管理¶
深入探讨 WebKit 所使用的内存管理系统。
概览¶
在 WebKit 中,当一个对象被另一个对象拥有时,我们通常使用 std::unique_ptr
来表示该所有权。WebKit 在其他情况下使用两种主要的管理策略:垃圾回收和引用计数。
WebKit 中的引用计数¶
概览¶
大多数 WebCore 对象不由 JavaScriptCore 的垃圾回收器管理。相反,我们使用引用计数。我们有两种引用计数指针类型:RefPtr
和 Ref
。RefPtr 旨在行为类似于 C++ 指针,而 Ref 旨在行为类似于 C++ 引用,这意味着前者可以设置为 nullptr
,但后者不能。
Ref<A> a1; // This will result in compilation error.
RefPtr<A> a2; // This is okay.
Ref<A> a3 = A::create(); // This is okay.
a3->f(); // Calls f() on an instance of A.
A* a4 = a3.ptr();
a4 = a2.get();
与 C++ 的std::shared_ptr
不同,引用计数的实现是托管对象的一部分。对象要与 RefPtr
和 Ref
一起使用的要求如下:
- 它实现了
ref()
和deref()
成员函数 - 每次调用
ref()
和deref()
将增加和减少其内部引用计数器 - 对
ref()
的初始调用隐含在new
中,即在对象被分配并调用构造函数之后;这意味着引用计数从 1 开始。 - 当其内部引用计数器达到 0 时调用
deref()
时,“this”对象将被析构并删除。
有一个方便的超模板类 RefCounted<T>
,它会自动为任何继承类 T 实现此行为。
如何使用 RefPtr 和 Ref¶
当通过 new 创建一个实现了 RefPtr 和 Ref 所需语义的对象时,我们必须立即使用 adoptRef
将其采纳为 Ref
类型,如下所示
class A : public RefCounted<A> {
public:
int m_foo;
int f() { return m_foo; }
static Ref<A> create() { return adoptRef(*new A); }
private:
A() = default;
};
这将在不调用新创建对象的 ref()
的情况下创建 Ref
实例,从而避免了不必要的从 0 到 1 的增量。WebKit 的编码约定是将构造函数设为私有,并添加一个静态 create
函数,该函数在采纳后返回引用计数对象的实例。
请注意,由于 C++11 中的拷贝省略,返回 RefPtr 或 Ref 是高效的,并且以下示例不会使用拷贝构造函数创建临时 Ref 对象)。
Ref<A> a = A::create();
将引用计数对象的所有权传递给函数时,使用右值引用和 WTFMove
(等效于带有一些安全检查的 std::move
),并且当调用者保证使对象保持活动状态时,使用常规引用,如下所示
class B {
public:
void setA(Ref<A>&& a) { m_a = WTFMove(a); }
private:
Ref<A> m_a;
};
...
void createA(B& b) {
b.setA(A::create());
}
请注意,由于拷贝省略,A::create
上没有 WTFMove
。
转发 ref 和 deref¶
如上所述,使用 RefPtr
和 Ref
管理的对象不一定必须继承自 RefCounted
。一种常见的替代方法是将 ref
和 deref
调用转发到拥有所有权的其他对象。例如,在以下示例中,Parent
类拥有 Child
类。当有人将 Child
存储在 Ref
或 RefPtr
中时,Parent
的引用计数会代表 Child
增加和减少。当指向任一对象的最后一个 Ref
或 RefPtr
消失时,Parent
和 Child
都会被析构。
class Parent : RefCounted<Parent> {
public:
static Ref<Parent> create() { return adoptRef(*new Parent); }
Child& child() {
if (!m_child)
m_child = makeUnique<Child>(*this);
return m_child
}
private:
std::unique_ptr<Child> m_child;
};
class Child {
public:
ref() { m_parent.ref(); }
deref() { m_parent.deref(); }
private:
Child(Parent& parent) : m_parent(parent) { }
friend class Parent;
Parent& m_parent;
}
引用循环¶
当对象 X 持有指向另一个对象 Y 的 Ref
或 RefPtr
,而对象 Y 又通过 Ref
或 RefPtr
拥有 X 时,就会发生引用循环。例如,以下代码会导致一个简单的内存泄漏,因为 A 持有 B 的 Ref
,而 B 又持有 A 的 Ref
class A : RefCounted<A> {
public:
static Ref<A> create() { return adoptRef(*new A); }
B& b() {
if (!m_b)
m_b = B::create(*this);
return m_b.get();
}
private:
Ref<B> m_b;
};
class B : RefCounted<B> {
public:
static Ref<B> create(A& a) { return adoptRef(*new B(a)); }
private:
B(A& a) : m_a(a) { }
Ref<A> m_a;
};
我们对 WebCore 中垃圾回收的对象需要特别小心,因为它们通常会在 C++ 代码中没有任何 Ref
或 RefPtr
的情况下使其他引用计数的 C++ 对象保持活动状态。因此,在 WebCore 代码中强引用 JS 值几乎总是错误的。
ProtectedThis 模式¶
因为 WebCore 中的许多对象都由树形数据结构管理,所以操作此类树形数据结构节点的函数最终可能会删除自身(this
对象)。这是非常不希望的,因为此类代码通常最终会出现使用后释放的错误。
为了防止这类错误,我们通常采用一种策略,即添加 Ref
或 RefPtr
类型的 protectedThis
局部变量,并如下所示存储 this
对象
ExceptionOr<void> ContainerNode::removeChild(Node& oldChild)
{
// Check that this node is not "floating".
// If it is, it can be deleted as a side effect of sending mutation events.
ASSERT(refCount() || parentOrShadowHostNode());
Ref<ContainerNode> protectedThis(*this);
// NotFoundError: Raised if oldChild is not a child of this node.
if (oldChild.parentNode() != this)
return Exception { NotFoundError };
if (!removeNodeWithScriptAssertion(oldChild, ChildChange::Source::API))
return Exception { NotFoundError };
rebuildSVGExtensionsElementsIfNecessary();
dispatchSubtreeModifiedEvent();
return { };
}
在这段代码中,移除 oldChild
的操作可以执行任意 JavaScript 并删除 this
对象。因此,如果我们没有 protectedThis
,那么在 this
对象已经被释放后,rebuildSVGExtensionsElementsIfNecessary
或 dispatchSubtreeModifiedEvent
可能会被调用。而 protectedThis
保证了该对象的引用计数至少为 1(因为 Ref 的构造函数将引用计数增加了 1)。
这种模式也可用于其他需要在代码块内防止被析构的对象。在以下代码中,childToRemove
是使用 C++ 引用传入的。因为此函数将从 this
容器节点中移除此子节点,所以它可能会在函数仍在运行时被析构。为了防止出现任何使用后释放的错误,此函数将其存储在 Ref(protectedChildToRemove
)中,这保证了该对象在函数将控制权返回给调用者之前保持活动状态
ALWAYS_INLINE bool ContainerNode::removeNodeWithScriptAssertion(Node& childToRemove, ChildChangeSource source)
{
Ref<Node> protectedChildToRemove(childToRemove);
ASSERT_WITH_SECURITY_IMPLICATION(childToRemove.parentNode() == this);
{
ScriptDisallowedScope::InMainThread scriptDisallowedScope;
ChildListMutationScope(*this).willRemoveChild(childToRemove);
}
..
另请参阅Darin 的 RefPtr 基础知识以获取更多信息。
WebKit 中的弱指针¶
在某些情况下,需要表达两个对象之间的关系,而无需将其生命周期绑定在一起。在这些情况下,WeakPtr
很有用。与 std::weak_ptr
类似,此类创建对对象的非拥有引用。有许多遗留代码为此目的使用原始指针,但目前正在努力始终使用 WeakPtr 代替,因此请在您编写的新代码中这样做。
要创建对象的 WeakPtr
,我们需要使其类继承自 CanMakeWeakPtr
,如下所示
class A : CanMakeWeakPtr<A> { }
...
function foo(A& a) {
WeakPtr<A> weakA = a;
}
当引用的对象被删除时,解引用 WeakPtr
将返回 nullptr
。因为创建 WeakPtr
会分配一个额外的 WeakPtrImpl
对象,所以您仍然有责任在适当的时候处理 WeakPtr
。
WeakHashSet¶
虽然普通的 HashSet
不支持将 WeakPtr
作为其元素,但有一个专门的 WeakHashSet
类,它支持弱引用一组元素。因为当引用的对象被删除时,WeakHashSet
不会收到通知,所以 WeakHashSet
的用户/所有者仍有责任从集合中删除相关条目。否则,WeakHashSet
将一直持有 WeakPtrImpl
,直到调用 computeSize
或发生重新哈希。
WeakHashMap¶
与 WeakHashSet
类似,WeakHashMap
是一个专门的类,用于将 WeakPtr
键映射到值。因为当引用的对象被删除时,WeakHashMap
不会收到通知,所以 WeakHashMap
的用户/所有者仍有责任从映射中删除相关条目。否则,WeakPtrImpl
及其值所使用的内存空间将不会被释放,直到下一次重新哈希或摊销清理周期到来(基于读写操作的总数)。
DOM 节点的引用计数¶
Node
是一个引用计数对象,但有点特殊。它有一个单独的布尔标志,指示它是否具有父节点。只要 Node
对象的引用计数大于 0 或设置了此布尔标志,它就不会被删除。布尔标志有效地充当了从父 Node
到其每个子 Node
的 RefPtr
。我们这样做是因为 Node
只知道其第一个子节点和最后一个子节点,并且每个兄弟节点都实现为双向链表,以实现兄弟节点的高效插入、删除和遍历。
从概念上讲,每个 Node
都由其根节点和对其的外部引用保持活动状态,我们将根节点用作每个 Node
的 JS 包装器的不透明根。因此,只要节点本身或任何共享相同根节点的其他节点被垃圾回收器访问,每个 Node
的 JS 包装器就会保持活动状态。
另一方面,Node
不会通过引用计数或布尔标志使其父节点或任何包含 Shadow 的祖先 Node
保持活动状态,尽管 JavaScript API 要求如此。为了实现这种 DOM API 行为,如果尚未有 JS 包装器,WebKit 将为每个正在从其父节点中移除的 Node
创建一个 JS 包装器。作为根节点(新移除的子树)的 Node
是其 JS 包装器的不透明根,如果移除的子树中存在任何需要保持活动的 JS 包装器,垃圾回收器将访问此不透明根。实际上,如果新移除的子树包含任何带有活动 JS 包装器的节点,这会使新的根节点及其所有后代节点保持活动状态,从而保留了 API 契约。
重要的是要认识到,在 Node
子类或由 Node
直接拥有的对象中存储指向另一个 Node
的 Ref
或 RefPtr
可能会创建引用循环,或者一个永远不会被清除的引用。无法保证每个节点将来都会从Document
中断开连接,并且某些 Node
只要存在,就可能始终具有父节点或子节点。只有在时间上受限制的情况下,才允许在 Node
子类或其拥有的其他数据结构中存储指向另一个 Node
的 Ref
或 RefPtr
。例如,可以将 Ref
或 RefPtr
存储在排队的事件循环任务中。在所有其他情况下,应使用 WeakPtr
引用另一个 Node
,并且应使用 JS 包装器关系(例如不透明根)来保持 Node
对象之间的生命周期联系。
同样重要的是要指出,通过在排队的事件循环任务中存储 Ref
或 RefPtr
来保持 C++ Node 对象活动,并不能使其 JS 包装器保持活动,并可能导致概念上活动的对象的 JS 包装器被错误地垃圾回收。为了避免这个问题,请改用GCReachableRef
,以在一段时间内临时持有对节点的强引用。例如,HTMLTextFormControlElement::scheduleSelectEvent()
使用 GCReachableRef
在事件循环任务中触发事件
void HTMLTextFormControlElement::scheduleSelectEvent()
{
document().eventLoop().queueTask(TaskSource::UserInteraction, [protectedThis = GCReachableRef { *this }] {
protectedThis->dispatchEvent(Event::create(eventNames().selectEvent, Event::CanBubble::Yes, Event::IsCancelable::No));
});
}
另一种方法是,我们可以使其继承自活动 DOM 对象,并使用以下函数之一来排队任务或事件
queueTaskKeepingObjectAlive
queueCancellableTaskKeepingObjectAlive
queueTaskToDispatchEvent
queueCancellableTaskToDispatchEvent
Document
节点还有一个特殊之处,因为每个Node
都可以通过ownerDocument 属性
访问文档,无论该 Node
是否连接到文档。每个文档都有外部客户端使用的常规引用计数和引用节点计数。文档的引用节点计数是其 ownerDocument
是该文档的节点总数。只要文档的引用计数和节点引用计数大于 0,它就会被保持活动状态。此外,当常规引用计数变为 0 时,它会清除各种状态,包括对拥有节点的内部引用,以断开与它们的任何引用循环。从这个意义上说,文档是特殊的,它可以存储指向其他节点的 RefPtr
。请注意,虽然引用节点计数作用类似于从每个 Node
到其所有者 Document
的 Ref
,但存储指向相同文档或任何其他文档的 Ref
或 RefPtr
将创建引用循环,应避免这样做,除非像上面提到的那样,它在时间上是有限的。