Automatic Reference Counting
How Swift knows when an object is no longer needed — and why it can be wrong.
Every class instance in Swift carries a count of how many strong references point at it. Create a reference, the count goes up. Let one go out of scope or reassign it, the count goes down. When the count hits zero, the object is deallocated immediately — deinit runs, and the memory is freed.
This is ARC, and "automatic" means the compiler inserts the counting for you, not that it thinks about ownership on your behalf. It's not a garbage collector: there's no background sweep, no pause, and no detection of unreachable islands. The rule is purely local — count reached zero, free it.
That immediacy is a real advantage, and the missing island detection is the one hole you have to cover yourself.
deinit runs the instant the last strong reference disappears — not later.final class Person { let name: String init(name: String) { self.name = name print("\(name) initialised") } deinit { print("\(name) deallocated") }}
do { let ada = Person(name: "Ada") // count = 1 let alias = ada // count = 2 _ = alias} // both go out of scope → count = 0 → deinit
// prints:// Ada initialised// Ada deallocated
Show all 18 linesShow less
final class Person { let name: String init(name: String) { self.name = name print("\(name) initialised") } deinit { print("\(name) deallocated") }}
do { let ada = Person(name: "Ada") // count = 1 let alias = ada // count = 2 _ = alias} // both go out of scope → count = 0 → deinit
// prints:// Ada initialised// Ada deallocatedTwo variables hold strong references to a Person object, giving it a retain count of two. As each variable goes out of scope the count drops, and at zero the object is deallocated.
Creating the instance takes the count to 1.
- Creating the instance takes the count to 1.
- Assigning to another variable takes it to 2.
- Both references go out of scope. Count hits 0, deinit runs immediately.
Under the hood
The compiler emits swift_retain and swift_release calls around reference assignments, and the count lives in the object's header next to its type metadata.
Three details worth knowing:
The optimiser removes most of them. ARC traffic you'd expect from reading the source is largely elided when the compiler can prove a reference outlives its uses. What's left is usually at ownership boundaries.
There are two counts. A strong count and an unowned count. When the strong count hits zero the object is deinitialised and its stored properties released, but the memory isn't returned while unowned references still exist — it becomes a zombie whose slot is reserved so that touching it can trap deterministically rather than reading someone else's data.
weak costs more than strong. Weak references go through a side table so they can all be zeroed when the object dies. Reading a weak reference is not free, which is why unowned exists as the cheaper option for cases where you can guarantee the lifetime.
Quick check
Which of these does NOT increase an object's strong reference count?
- Assigning it to a new `let` constant
- Capturing it strongly in an escaping closure
- Assigning it to a `weak var`
- Storing it in an array
Reveal answer
Assigning it to a `weak var`
weak and unowned both deliberately avoid incrementing the strong count — that's their entire purpose. Everything else that makes a strong reference bumps it, including closure captures and collection storage, which is why an array of view controllers keeps them all alive. weak additionally registers in a side table so the reference can be zeroed to nil when the object dies; unowned doesn't, which is why it traps instead.
Interview question
Explain how ARC decides when to free an object, and where that logic falls short.
Think first, then reveal
Each class instance has a strong reference count in its header. The compiler inserts retains and releases around assignments, captures and scope exits. When a release takes the count to zero, deinit runs synchronously on that thread and the memory is freed.
The important properties are that it's deterministic — no pause, no collector, deallocation happens at a point you can reason about — and that it's *local*. The decision only ever looks at one object's count.
That locality is where it falls short. A group of objects that reference each other keeps every count above zero, so the whole island is leaked even though nothing outside can reach it. A tracing garbage collector would find that by walking from the roots and noticing the island is unreachable; ARC has no roots and does no walking, so it can't. Breaking the cycle with weak or unowned on one edge is the developer's responsibility.
The trade is deliberate: predictable, low-latency deallocation and no runtime pauses, in exchange for having to think about ownership at cycle-shaped relationships.
What an interviewer listens for
- Describes the count, and that deinit is synchronous and immediate
- Frames the limitation as locality — no reachability analysis
- Contrasts with tracing GC accurately rather than vaguely
- Presents it as an explicit trade-off, not a flaw
Key takeaway
ARC frees an object the moment its strong count hits zero — deterministically, with no collector. It cannot see cycles, so every strong reference you create is a lifetime decision you own.