7. Memory management & ARC¶
What & why¶
Swift has no garbage collector and no manual free. Instead it uses Automatic Reference Counting (ARC): every class instance tracks how many strong references point to it, and is destroyed the moment that count hits zero. It's deterministic — deinit runs at a predictable time — but you must avoid reference cycles, which ARC can't break.
Value types (structs, enums, tuples) aren't reference counted: they live inline on the stack or inside their owner and are freed when that goes away. ARC concerns classes, closures, and actors.
How ARC counts¶
class Person {
let name: String
init(name: String) { self.name = name; print("\(name) created") }
deinit { print("\(name) destroyed") }
}
var ref1: Person? = Person(name: "Ada") // count = 1 → "Ada created"
var ref2 = ref1 // count = 2
ref1 = nil // count = 1
ref2 = nil // count = 0 → "Ada destroyed"
The compiler inserts the retain/release calls. You see only the results.
Scope matters too:
func scoped() {
let p = Person(name: "Grace") // created
print("using \(p.name)")
} // p goes out of scope → destroyed
scoped()
The optimizer may release an object earlier than the end of scope, right after its last use, so don't depend on a variable keeping an object alive. If you need that, use withExtendedLifetime(p) { ... }.
Reference cycles¶
Two objects that hold each other strongly can never reach zero:
class Author {
var book: Book?
deinit { print("Author freed") }
}
class Book {
var author: Author?
deinit { print("Book freed") }
}
var a: Author? = Author()
var b: Book? = Book()
a?.book = b
b?.author = a
a = nil
b = nil
// Nothing printed — both objects leaked!
weak references¶
A weak reference does not increase the count. It's automatically set to nil when the target is destroyed, so it must be an optional var:
class Book2 {
weak var author: Author2? // breaks the cycle
deinit { print("Book2 freed") }
}
class Author2 {
var book: Book2?
deinit { print("Author2 freed") }
}
var a2: Author2? = Author2()
var b2: Book2? = Book2()
a2?.book = b2
b2?.author = a2
a2 = nil // Author2 freed (nothing else owns it)
print(b2?.author as Any) // nil — weak ref was cleared
b2 = nil // Book2 freed
Use weak when the other object may legitimately go away first (delegates, parent back-references, observers).
unowned references¶
unowned doesn't retain, isn't optional, and crashes if used after the target is gone. Use it only when the referenced object is guaranteed to outlive you:
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
}
class CreditCard {
let number: String
unowned let customer: Customer // a card can't exist without its customer
init(number: String, customer: Customer) { self.number = number; self.customer = customer }
}
Rule: if in doubt, choose weak. A nil is easier to handle than a crash.
Closures and capture lists¶
Closures that capture self and are stored on self form a cycle:
class ViewModel {
var title = "Hello"
var onChange: (() -> Void)?
func bind() {
onChange = { [weak self] in // weak capture breaks the cycle
guard let self else { return }
print(self.title)
}
}
deinit { print("ViewModel freed") }
}
You don't always need [weak self]:
- Non-escaping closures (
map,filter,forEach) can't outlive the call — no cycle. - One-shot escaping closures that don't get stored on
self(aTaskthat finishes,DispatchQueue.main.async) only extend the lifetime until they run; a cycle needs a stored loop. - When you want the object kept alive until the work finishes, strong capture is correct.
Capture lists can capture more than self:
let vm = ViewModel()
let name = "Ada"
let c = { [name, weak vm] in print(name, vm?.title ?? "gone") } // snapshot one, weak another
Diagnosing leaks¶
- Add
deinit { print(...) }to suspect classes. If it never prints, something still holds a strong reference. - Xcode's Memory Graph Debugger and Instruments → Leaks show cycles visually (on Apple platforms).
- On any platform,
isKnownUniquelyReferenced(&obj)tells you if exactly one reference exists, andCFGetRetainCount(Apple) exposes counts for experiments. - Heap growth in a long-running tool? Suspect closures stored in arrays/dictionaries and observer lists that never get removed.
Value types and ARC¶
Structs that contain class references copy the reference, not the object:
struct Wrapper {
var person: Person // reference inside a value type
}
var w1 = Wrapper(person: Person(name: "Linus"))
var w2 = w1 // copies the struct; both share the same Person
w2.person = Person(name: "Ken") // reassign w2's reference only
A struct is only as "value-like" as its stored properties. Include a class property and you inherit its reference semantics.
Copy-on-write¶
Array, Dictionary, String, and Set are structs that wrap a reference-counted buffer. Copying shares the buffer; the first mutation of a non-uniquely-referenced copy makes a real copy:
var original = [1, 2, 3]
var copy = original // O(1): shares storage
copy.append(4) // now copy gets its own storage
You can implement the same for your own types with isKnownUniquelyReferenced — rarely needed.
Unmanaged memory (a peek)¶
For C interop and low-level work there are UnsafePointer, UnsafeMutablePointer, and Unmanaged<T> — escape hatches that bypass ARC and safety. See the Advanced level.
Common mistakes¶
- Delegate properties declared strong. Declare them
weak var delegate: SomeDelegate?(and make the protocolAnyObject-constrained). - Parent ↔ child links both strong — make the back-reference
weak. - Blanket
[weak self]everywhere, leading to silent early exits (guard let self else { return }) when the work should have completed. Think about ownership instead. unownedwith uncertain lifetimes — crashes in production.- Capturing
selfin aTimer/NotificationCenterclosure and never invalidating it/removing the observer. - Assuming
deinitordering across objects — release order is deterministic per reference but not something to build designs on.
Exercises¶
- Reproduce a retain cycle between
ParentandChildclasses. Prove it withdeinitprints, then break it withweak. - Create a
Nodeclass for a doubly-linked list; decide which ofnext/prevshould be weak. - Write a class with a stored closure property and demonstrate the cycle and fix.
- Use
isKnownUniquelyReferencedto implement a copy-on-writeBufferstruct wrapping a class. - Make a
Customer/CreditCardpair like above and print when each is freed. - Show that a closure passed to
mapthat capturesselfdoesn't leak.
Recap¶
- ARC retains/releases class instances automatically;
deinitruns when the count hits zero. - Strong cycles leak. Break them with
weak(optional, auto-nils) orunowned(non-optional, crashes if wrong). - Closures capture by reference; use capture lists (
[weak self]) for stored escaping closures. - Structs aren't reference counted, but any class reference inside them is.
- Verify with
deinitprints and the memory-graph tools.