8-7.【内存管理机制】strong、weak、unowned 引用在底层是如何存储和管理的?

21 阅读5分钟

在 Swift 的底层实现中,这三种引用的区别远不止是“自动置空”那么简单。它们实际上是通过操作 HeapObject 的位域(Bitfields)和引入 Side Table(侧表) 机制来管理的。


1. Strong(强引用):内联计数

强引用是 Swift 最基础的内存管理方式。

  • 存储位置:直接存储在对象头部的 RefCounts 字段中。
  • 底层机制:当产生强引用时,运行时调用 swift_retain(),它利用原子操作递增 64 位复合字中的 Strong Extra RefCount 部分。
  • 性能:极高。它是一次原子指令操作,不涉及额外的内存分配。

2. Weak(弱引用):侧表(Side Table)

弱引用是最复杂的。为了实现“对象销毁后自动置空(Zeroing)”,Swift 引入了侧表。

  • 存储位置:弱引用指针并不直接指向对象,而是指向该对象的 Side Table

  • 侧表的诞生:当一个对象第一次被 weak 引用时,系统会在堆上分配一个 HeapSideTableEntry。原对象头部的 RefCounts 会被修改,其中一个特定的位(Bit)会被标记,其余位存储侧表的内存地址。

  • 工作流程

    1. 对象销毁(强引用归零)后,对象内存被释放,但侧表不会立即消失
    2. 当你在代码中访问弱引用时,程序先跳转到侧表,检查侧表里的“强引用计数”。
    3. 如果发现强引用已归零,则返回 nil
    4. 只有当所有弱引用也都消失后,侧表才会被回收。

3. Unowned(无主引用):半销毁状态

unowned 是强引用和弱引用的折中方案,它在安全性和性能之间做了平衡。

  • 存储位置:同样存储在对象头部的 RefCounts 位域中(Unowned RefCount)。

  • 底层机制:它不增加强引用计数,但会增加无主引用计数。

  • 生命周期(Zombie 状态)

    • 当强引用归零时,对象会执行 deinit 并释放其持有的资源,但对象的物理内存(HeapObject 结构)不会被立即回收
    • 此时对象处于“僵尸”状态。如果此时访问 unowned 引用,运行时会检查计数器并发现对象已析构,随后直接触发 Runtime Trap(崩溃)
    • 只有当无主引用计数也归零时,这块物理内存才会被真正 free

4. 深度对比表

特性StrongWeakUnowned
引用计数位置对象头部 (Inline)Side Table (侧表)对象头部 (Inline)
对象销毁后内存立即回收侧表保留,指针变 nil内存保留,直到无主计数归零
访问安全性绝对安全返回可选值 (nil)触发崩溃 (Runtime Trap)
性能开销极低 (原子操作)较高 (多层间接寻址)低 (原子操作,优于 weak)
内存溢出风险循环强引用导致泄漏悬垂指针访问导致崩溃

5. 什么时候该选哪种?

  • Strong:默认选择,用于表达“拥有”关系。
  • Weak:用于可能在生命周期内变为 nil 的引用。典型的如 delegateIBOutlet 以及闭包中的 [weak self]。它最安全,但略慢。
  • Unowned:用于确定引用对象生命周期等于或长于当前对象的场景。例如,一个 CreditCard 实例引用它的 User。它性能更好,且不需要处理可选值(Optional)。

英文版

8-7. [Memory Management] How are Strong, Weak, and Unowned references stored and managed at the low level?

In Swift's underlying implementation, the difference between these references goes far beyond "automatic nil-ing." They are managed through sophisticated manipulations of the HeapObject bitfields and the introduction of the Side Table mechanism.


1. Strong Reference: Inline Counting

Strong references are the most fundamental way Swift manages memory.

  • Storage Location: Stored directly within the RefCounts field of the object's header.
  • Low-level Mechanism: When a strong reference is created, the runtime calls swift_retain(). This uses atomic operations to increment the Strong Extra RefCount portion of a 64-bit composite word in the header.
  • Performance: Extremely high. it is a single atomic instruction and involves no extra memory allocation.

2. Weak Reference: The Side Table

Weak references are the most complex. To implement "automatic zeroing" after an object is destroyed, Swift introduces the Side Table.

  • Storage Location: A weak pointer does not point directly to the object. Instead, it points to the object's Side Table (HeapSideTableEntry).

  • Birth of a Side Table: When an object is first referenced by a weak pointer, the system allocates a HeapSideTableEntry on the heap. The original object's RefCounts header is modified: a specific bit is toggled to indicate a side table exists, and the remaining bits store the memory address of that side table.

  • Workflow:

    1. After the object is destroyed (strong count reaches zero), the object's memory is deallocated, but the Side Table remains.
    2. When you access the weak reference in your code, the program jumps to the Side Table to check the "Strong Reference Count."
    3. If the strong count is zero, it returns nil.
    4. The Side Table is only reclaimed once all weak references to it have also vanished.

3. Unowned Reference: The "Zombie" State

unowned is a middle ground between strong and weak references, balancing performance and safety.

  • Storage Location: Also stored within the object's header bitfields (Unowned RefCount).

  • Low-level Mechanism: It does not increment the strong reference count, but it increments the unowned reference count.

  • Lifecycle (The Zombie State) :

    1. When the strong count reaches zero, the object executes deinit and releases its held resources, but the physical memory of the object (the HeapObject structure) is not immediately freed.
    2. The object is now in a "Zombie" state. If you access the unowned reference now, the runtime checks the counter, sees the object has been de-initialized, and triggers a Runtime Trap (Crash) .
    3. The physical memory is only truly free-ed once the unowned reference count also reaches zero.

4. Deep Comparison Table

FeatureStrongWeakUnowned
Count LocationObject Header (Inline)Side Table (External)Object Header (Inline)
After DeallocationMemory freed immediatelySide Table remains; pointer becomes nilMemory remains until unowned count is 0
Access SafetyGuaranteed safeReturns Optional (nil)Triggers Runtime Trap (Crash)
Performance CostMinimal (Atomic op)Higher (Multiple indirections)Low (Atomic op, better than weak)
RiskStrong cycles lead to leaksNoneDangling pointer access leads to crash

5. When to choose which?

  • Strong: The default choice, used to express an "ownership" relationship.
  • Weak: Use for references that may become nil during their lifecycle. Typical cases include delegates, IBOutlets, and [weak self] in closures. It is the safest but slightly slower.
  • Unowned: Use when you are certain the referenced object's lifecycle is equal to or longer than the current object. For example, a CreditCard instance referencing its User. It offers better performance and avoids the need to handle Optionals.