Every class instance has a reference count, incremented each time a strong reference (the default kind) points to it, decremented when that reference goes away; the instance deallocates deterministically the moment its count reaches zero — no separate GC thread, no unpredictable pause. Structs and enums, being value types, aren't managed by ARC at all. The real, common failure mode is a retain cycle: two instances holding strong references to each other never reach a zero count, since each keeps the other alive, leaking both permanently even when nothing else in the program references either one. weak (an optional reference that automatically becomes nil when the referenced object deallocates) and unowned (a non-optional reference that assumes the object will always outlive it, crashing if accessed after deallocation) both break the cycle by making one side of the relationship not count toward the reference total.
ARC's basic mechanism
deinit runs the instant the reference count hits zero — deterministic, immediate, and directly observable, unlike a tracing garbage collector's unpredictable timing.
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) is being deallocated") }
}
var person: Person? = Person(name: "Alice")
person = nil // reference count hits 0 — deinit runs IMMEDIATELY, prints "Alice is being deallocated"A real retain cycle: two classes leaking each other
Setting both to nil does NOT trigger deinit for either — alice still holds a strong reference to the apartment, and the apartment still holds a strong reference back to alice, so neither's count ever reaches zero.
class Person {
let name: String
var apartment: Apartment?
init(name: String) { self.name = name }
deinit { print("\(name) deallocated") }
}
class Apartment {
var tenant: Person? // STRONG reference back to Person — this creates the cycle
deinit { print("Apartment deallocated") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment()
alice?.apartment = apt
apt?.tenant = alice
alice = nil
apt = nil
// NEITHER deinit ever runs — both leaked forever, each keeping the other aliveFixing it: weak on one side
tenant is now weak — it doesn't count toward Person's reference total, so setting alice to nil correctly drops Person's count to zero, deinit runs, and the cycle is broken.
class Apartment {
weak var tenant: Person? // weak — doesn't keep Person alive, automatically becomes nil
deinit { print("Apartment deallocated") }
}
// with this fix, both deinit calls now run correctly when alice and apt are set to nil