오늘은 weak과 unowned 차이점에 대해서 알아보려고 합니다.
이 포스팅은 두 키워드를 사용하면 레퍼런스 카운트가 되지 않는 공통점이 있는데 어떤 이유에서 해당 인스턴스가 해제될 때에는 차이점을 가지고 있는지에 대한 궁금증으로부터 시작되었습니다.
strong, weak, unowned와 ARC에 대해서 아직 잘 모르겠다면 아래 포스팅을 참고하시면 됩니다!
ARC 포스팅
weak, unowned는 강한 참조를 유지하지 않기 때문에 메모리에 인스턴스를 유지할 수 없다는 공통점이 있습니다.
차이점은 메모리에서 인스턴스 해제 후에 weak는 자동으로 nil이 할당됩니다. 따라서 weak는 항상 optional type으로 선언해야 합니다.
unowned 존재하지 않는 객체의 메모리를 참조하게 되는 dangling pointer가 됩니다. (Swift 5 이후, unowned도 optional 타입을 사용할 수 있게 됨)
| weak | unowned | |
|---|---|---|
| 공통점 | reference count가 되지 않는다. | reference count가 되지 않는다. |
| 차이점 | 메모리가 해제될 때 자동으로 nil로 초기화 된다. | 메모리가 해제되면 dangling pointer를 갖는다. |
이러한 차이점이 생기는 이유는 무엇일까요?
그 이유는 바로 객체를 참조하는 방식이 다르기 때문입니다.
unowned는 객체를 직접 참조하는 반면 weak는 Side Table을 참조합니다.
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h
Swift 공식 레포에 있는 RefCount.h 파일에서 로우 레벨에서 reference count가 strong, weak, unowned 별로 어떤 방식으로 이루어지는지 알아보려고 합니다.
An object conceptually has three refcounts. These refcounts are stored either "inline" in the field following the isa or in a "side table entry" pointed to by the field following the isa.
객체는 개념적으로 3가지 refcount를 가지고 있다. 이 refcount는 객체 안에서 inline으로 저장되거나 side table entry로 저장된다.
The strong RC counts strong references to the object. When the strong RC reaches zero the object is deinited, unowned reference reads become errors, and weak reference reads become nil.
The strong RC is stored as an extra count: when the physical field is 0 the logical value is 1.
The unowned RC counts unowned references to the object. The unowned RC also has an extra +1 on behalf of the strong references; this +1 is decremented after deinit completes. When the unowned RC reaches zero the object's allocation is freed.
The weak RC counts weak references to the object. The weak RC also has an extra +1 on behalf of the unowned references; this +1 is decremented after the object's allocation is freed. When the weak RC reaches zero the object's side table entry is freed.
Weak RC에서 side table 내용이 나옵니다. 여기까지는 그냥 weak와 side table이 연관되어 있구나 생각하면 되겠다.
또 기억해야 할 건 객체에 대해서 denit, allocation freed 등 여러 상태가 존재한다는 것이다. 우리가 생각한 것보다 객체가 생성되고 할당 해제되기 까지 다양한 상태를 갖구나 예상할 수 있을 것이다.

사이드 테이블은 Swift에서 weak reference를 구현하기 위한 메커니즘이다.
Objects initially start with no side table. They can gain a side table when:
- a weak reference is formed and pending future implementation:
- strong RC or unowned RC overflows (inline RCs will be small on 32-bit)
- associated object storage is needed on an object
- etc
Gaining a side table entry is a one-way operation; an object with a side table entry never loses it. This prevents some thread races. Strong and unowned variables point at the object.
Weak variables point at the object's side table.
객체는 처음에는 사이드 테이블을 가지지 않는다. 사이드 테이블이 생성되는 경우는 다음과 같다.
사이드 테이블 엔트리가 생성되면, 객체는 사이드 테이블을 절대 잃지 않는다.
이는 thread의 race condition를 막는다고 한다 .
우리는 Refcount가 객체 안에서 inline으로 저장되거나 side table entry로 저장된다는 걸 알았다. 각각의 방식이 어떻게 저장되는지 알아보자.
HeapObjectHeapObjec 는 Heap에 저장되는 동적으로 할당되는 객체의 구조를 나타낸 것이다.
다음과 같은 데이터를 포함한다.
isa: 타입에 대한 metadataInlineRefCounts: 인라인 참조 카운트 strong RC 와 unowned RC, Flag을 합한 값을 저장 or side table entry의 참조값을 저장HeapObjectSideTableEntry를 가리키게 된다. HeapObject {
isa
InlineRefCounts {
atomic<InlineRefCountBits> {
strong RC + unowned RC + flags
OR
HeapObjectSideTableEntry*
}
}
}
HeapObjectSideTableEntry객체의 Side Table 구조로, 객체가 인라인 참조 카운트를 넘어서 참조를 관리해야 할 때 사용된다.
다음과 같은 데이터를 포함한다.
object pointer: 객체의 포인터atomic<SideTableRefCountBits>: strong RC와 unowned RC, weak RC, flag를 합한 값HeapObjectSideTableEntry {
SideTableRefCounts {
object pointer
atomic<SideTableRefCountBits> {
strong RC + unowned RC + weak RC + flags
}
}
}
그럼 이제 객체가 저장되는 방식에 대해서 알아봤으니 객체가 어떤 과정으로 메모리에세 해제되는지 살펴보자.
객체의 life cycle은 아래 그림과 같다. object는 약한 참조가 있느냐 즉 side table 존재 여부에 따라 다른 life cycle을 갖는다. 이에 대해 자세히 알아보자.

이미지 출처: https://shubhamchawla00.medium.com/memory-management-on-ios-ad2244b49b20
The object is alive.
Object's refcounts are initialized as 1 strong, 1 unowned, 1 weak.
No side table. No weak RC storage.
Strong variable operations work normally.
Unowned variable operations work normally.
Weak variable load can't happen.
Weak variable store adds the side table, becoming LIVE with side table.
When the strong RC reaches zero deinit() is called and the object
becomes DEINITING.
객체가 side table 없이 존재하는 경우이다. 객체의 RC는 strong 1, unowned 1, weak 1로 초기화된다.
side table이 존재하지 않으므로 weak RC 저장소가 없다. → side table에서만 weak RC를 저장했었음
Strong과 Unowned 변수 연산이 정상적으로 동작된다.
약한 참조가 발생하면 객체의 상태가 Live with side table로 변합니다. 이후에는 weak reference 변수를 통해 객체에 접근할 수 있다.
strong RC가 0이 되면 deinit이 호출되고 객체는 DEINITING 된다.
Weak Reference 변수 작업이 정상적으로 작동하는 것을 제외하고는 LIVE without side table과 동일하다.
deinit() is in progress on the object.
Strong variable operations have no effect.
Unowned variable load halts in swift_abortRetainUnowned().
Unowned variable store works normally.
Weak variable load can't happen.
Weak variable store stores nil.
When deinit() completes, the generated code calls swift_deallocObject.
swift_deallocObject calls canBeFreedNow() checking for the fast path
of no weak or unowned references.
If canBeFreedNow() the object is freed and it becomes DEAD.
Otherwise, it decrements the unowned RC and the object becomes DEINITED.
객체에 대해 deinit()가 진행 중인 DEINITING 상태일 때 동작이다.
Strong Reference 변수는 아무런 영향을 받지 않는다.
Unowned 변수 로드는 swift_abortRetainUnowned 때문에 중단되지만 저장 작업은 정상적으로 동작된다.
weak 변수를 통해 객체를 불러올 수 없다 → 당연하다. side table이 없으니
weak 변수에 nil이 저장된다.
deinit()이 완료되면 생성된 코드는 swift_deallocObject를 호출한다.
swift_deallocObject는 canBeFreedNow()를 호출하여 weak나 unowned 참조가 있는지 확인한다.canBeFreedNow()가 호출되면 객체는 freed 되고 DEAD 상태가 된다.deinit()된 이후 1이 감소된다.( unowned RC 참고)Weak variable load returns nil.
Weak variable store stores nil.
canBeFreedNow() is always false, so it never transitions directly to DEAD.
Everything else is the same as DEINITING.
side table을 가지고 있는 경우 deinit() 진행 중일 때이다. weak 변수에 객체를 저장하거나 불러올 때 모두 nil을 받는다.
side table을 가지고 있다는 것은 추가적인 weak RC가 존재한다는 걸 의미한다. 따라서 canBeFreedNow()는 항상 false 값을 갖으므로 바로 DEAD 상태로 변할 수 없다.
deinit() has completed but there are unowned references outstanding.
Strong variable operations can't happen.
Unowned variable store can't happen.
Unowned variable load halts in swift_abortRetainUnowned().
Weak variable operations can't happen.
When the unowned RC reaches zero, the object is freed and it becomes DEAD.
side table이 없는 DEINITED 상태일 때 동작이다.
deinit()가 완료되었지만 unowned reference가 남아 있는 경우이다. (남아있지 않았으면 DEINITING에서 DEAD 상태가 되었을 것)
이미 DEINITING에서 ****Strong RC가 0이 되었으므로 강한 참조에 대한 연산이 일어나지 않는다.
Unowned RC가 비로소 0이 될 때, 객체가 free(완전히 해제)되고 상태는 DEAD가 됩니다.
Weak variable load returns nil.
Weak variable store can't happen.
When the unowned RC reaches zero, the object is freed, the weak RC is
decremented, and the object becomes FREED.
Everything else is the same as DEINITED.
side table이 있는 DEINITED 상태일 때 동작이다.
weak 변수를 통해 객체를 불러올 때 nil을 리턴하며 객체를 저장할 수 없다.
unowned RC가 0이 될 때, 객체는 free(완전히 해제)되고 weak RC도 감소하고 object가 FREED 상태가 된다.
This state never happens.
side table이 없는 경우 약한 참조가 존재하지 않으므로 바로 DEAD 상태가 된다.
따라서 절대 일어나지 않는다.
The object is freed but there are weak refs to the side table outstanding.
Strong variable operations can't happen.
Unowned variable operations can't happen.
Weak variable load returns nil.
Weak variable store can't happen.
When the weak RC reaches zero, the side table entry is freed and
the object becomes DEAD.
side table이 있는 FREED 상태일 때 동작이다.
FREED 상태는 객체는 해제되었는데 여전히 side table에 대한 weak RC가 남아있는 경우이다.
이때 객체를 weak로 참조하게 되면 nil을 리턴하고 객체를 저장할 수 없다.
weak RC가 0이 되면(모든 RC가 0) side table이 freed 되고 객체는 마침내 DEAD 상태가 된다.
The object and its side table are gone.
객체와 side table 모두 해제된 상태이다.
high-level에서 Swift에서 일어나는 Reference Counting 방식을 배울 때는 strong reference의 경우에만 count를 적용한다고 배웠다. 하지만 low-level을 살펴보니 전혀 그렇지 않았다.
Reference가 strong, unowned, weak에 상관없이 count를 저장하고 있었고 만약 weak 참조가 존재한다면 side table를 할당하여 reference count를 관리하고 있었다.
strong RC가 0이 되면 객체는 denit 되지만 unowned reference가 존재한다면 여전히 메모리에 남아있었다. (DENITED 상태)
unowned RC가 비로소 0이 될 때는 객체는 완전 해제되지만 weak RC가 남아있다면 side table이 아직 메모리에 남아있게 된다. (FREED 상태)
Discover Side Tables - Weak Reference Management Concept in Swift
https://github.com/swiftlang/swift/blob/main/stdlib/public/SwiftShims/swift/shims/RefCount.h