Polyglot
Languages Swift memory
Swift § memory

Memory and ARC

Swift uses Automatic Reference Counting (ARC) — an automatic memory-management mechanism that tracks the number of references to each instance of a reference type (class, actor) and deallocates it when the count reaches zero. Unlike a tracing garbage collector (Java, Go), ARC is deterministic — deallocation occurs immediately when the last reference is released. The principal mechanisms: strong (default — keeps the object alive), weak (does not keep alive; becomes nil when the object is deallocated), unowned (does not keep alive; expected to outlive the reference). The principal pitfall: retain cycles — circular strong references that prevent deallocation. The conventional defences: capture lists in closures, weak references in delegate patterns, unowned references when lifetime is guaranteed. The combination — automatic deterministic counting, the strong/weak/unowned distinction, capture lists for closures, the deinit for cleanup — is the substance of Swift’s memory model for reference types.

Value types (struct, enum) are not reference-counted — they are allocated inline (on the stack or as part of their containing context).

ARC basics

ARC tracks references to class instances:

class Person {
    let name: String

    init(name: String) {
        self.name = name
        print("\(name) is initialised")
    }

    deinit {
        print("\(name) is deinitialised")
    }
}

var p1: Person? = Person(name: "Alice")            // count: 1
var p2 = p1                                         // count: 2
var p3 = p1                                         // count: 3

p1 = nil                                            // count: 2
p2 = nil                                            // count: 1
p3 = nil                                            // count: 0 → "Alice is deinitialised"

When all references are gone, the object is deallocated and deinit runs.

The mechanism is deterministic — the deallocation point is predictable.

Strong references

The default — admitted without any keyword:

class Box {
    var value: Int = 0
}

var b1 = Box()                                     // strong reference; count: 1
let b2 = b1                                         // count: 2

The strong reference keeps the object alive. When all strong references are gone, the object is deallocated.

Retain cycles

A retain cycle — two or more objects strongly referencing each other:

class Apartment {
    var tenant: Person?                            // strong (default)

    deinit { print("Apartment deinitialised") }
}

class Person {
    var apartment: Apartment?                      // strong (default)

    deinit { print("Person deinitialised") }
}

var alice: Person? = Person()
var unit5: Apartment? = Apartment()

alice?.apartment = unit5                           // alice → unit5
unit5?.tenant = alice                              // unit5 → alice (cycle!)

alice = nil                                         // count of Person: 1 (still referenced by unit5)
unit5 = nil                                         // count of Apartment: 1 (still referenced by Person)
                                                    // BOTH objects leak — neither deinit prints

The cycle prevents deallocation; the objects leak. The conventional defences are weak and unowned.

Weak references

A weak reference admits referring to an object without incrementing the count:

class Apartment {
    weak var tenant: Person?                       // weak reference

    deinit { print("Apartment deinitialised") }
}

class Person {
    var apartment: Apartment?

    deinit { print("Person deinitialised") }
}

var alice: Person? = Person()
var unit5: Apartment? = Apartment()

alice?.apartment = unit5
unit5?.tenant = alice                              // weak; doesn't increment Person's count

alice = nil                                         // Person's count: 0 → deallocated
                                                    // unit5.tenant becomes nil automatically

unit5 = nil                                         // Apartment's count: 0 → deallocated

The weak reference:

  • Does not keep the object alive.
  • Becomes nil when the referenced object is deallocated.
  • Must be a var (not let).
  • Must be optional (must admit nil).

The conventional uses are delegate patterns, parent-child relationships (the child is weak-referenced from the parent context), and observation.

Unowned references

An unowned reference admits referring to an object without incrementing the count and is expected to always be valid:

class Customer {
    let name: String
    var card: CreditCard?

    init(name: String) { self.name = name }
    deinit { print("Customer deinitialised") }
}

class CreditCard {
    let number: String
    unowned let customer: Customer                 // unowned — card outlives card-customer association

    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }

    deinit { print("Card deinitialised") }
}

The unowned reference:

  • Does not keep the object alive.
  • Does not become nil — the reference is assumed to remain valid.
  • Crashes if accessed after the referenced object is deallocated.
  • Need not be optional.

The conventional uses are when the lifetime relationship is guaranteed — the referenced object will not be deallocated before the reference. The mechanism admits substantial performance over weak (no nil check) at the cost of some safety.

When to use which

ReferenceUse when
strong (default)The reference should keep the object alive
weakThe reference should not keep alive AND may become nil
unownedThe reference should not keep alive AND is guaranteed to outlive

The conventional Swift discipline:

  • Default to strong — it’s correct in most cases.
  • Use weak in delegate patterns and other “may outlive” relationships.
  • Use unowned sparingly — when the lifetime guarantee is genuinely solid.

Closures and capture lists

Closures may capture references; this admits retain cycles:

class ViewController {
    var onTap: (() -> Void)?
    var counter = 0

    func setupHandler() {
        onTap = {                                  // closure captures self strongly
            self.counter += 1
        }
    }

    deinit { print("ViewController deinit") }
}

let vc = ViewController()
vc.setupHandler()
// vc → onTap → self (vc) — cycle!

The conventional defence is capture lists:

func setupHandler() {
    onTap = { [weak self] in
        self?.counter += 1                         // optional access
    }
}

// Or unowned (when the handler can't outlive self):
func setupHandler() {
    onTap = { [unowned self] in
        self.counter += 1
    }
}

The capture list [weak self] or [unowned self] admits explicit reference behaviour.

For multiple captures:

closure = { [weak self, unowned manager] in
    self?.update(via: manager)
}

The conventional contemporary discipline:

  • [weak self] — the safer default for closures stored on self.
  • [unowned self] — only when the closure’s lifetime is genuinely scoped to self.

deinit

The deinit runs automatically when an instance is deallocated:

class FileHandle {
    let path: String
    private let descriptor: Int32

    init(path: String) throws {
        self.path = path
        self.descriptor = open(path, O_RDONLY)
        guard descriptor >= 0 else { throw IOError.openFailed }
    }

    deinit {
        close(descriptor)                          // cleanup runs automatically
    }
}

The mechanism admits deterministic resource cleanup; conventional for files, network connections, locks, etc.

deinit cannot:

  • Take parameters.
  • Return a value.
  • Be called explicitly.

It runs exactly once per instance, when the count reaches zero.

Capture by value vs reference

In closures:

  • Reference types (classes) are captured by reference — modifications to the same object are visible.
  • Value types are captured by value by default — but if the closure mutates the captured value, the closure captures by reference (sort of):
var counter = 0                                    // value type (Int)

let increment = { counter += 1 }                   // captures counter

increment()
increment()
print(counter)                                     // 2 — closure mutates captured counter

The mechanism admits substantial closures-as-state patterns.

For explicit value-capture:

let n = 42
let f = { [n] in print(n) }                       // captures n by value

unowned(unsafe) and unowned(safe)

Two forms of unowned:

  • unowned (alias unowned(safe)) — checks at runtime; crashes on access after deallocation.
  • unowned(unsafe) — does not check; undefined behaviour on access after deallocation.

The unsafe form admits substantial performance; conventional only when the lifetime is provably guaranteed and performance is critical.

ARC and threads

ARC operations are atomic — increment/decrement of reference counts is thread-safe. The mechanism admits multi-threaded code without explicit synchronisation around ARC.

For uncontended counts, the implementation is substantially efficient; contended counts (multiple threads holding references to the same object) admit substantial overhead.

The conventional discipline favours value types for thread-shared data — value types do not require ARC and admit independent copies.

Memory leaks and tools

ARC eliminates most memory leaks but admits leaks via:

  • Retain cycles — the principal source of leaks in Swift code.
  • Long-lived caches — substantial intentional retention.
  • Closures captured strongly without realising.
  • Notification center observers not removed.

The conventional debugging tools:

  • Xcode Memory Graph Debugger — visualises retained instances.
  • Instruments (Allocations, Leaks) — runtime profiling.
  • weak checks during code review — particularly around closure captures.

Common patterns

Delegate pattern with weak

protocol DataSourceDelegate: AnyObject {           // class-only; admits weak
    func dataSource(_ source: DataSource, didLoad data: [Item])
}

class DataSource {
    weak var delegate: DataSourceDelegate?         // weak — delegate may outlive

    func load() {
        // ...
        delegate?.dataSource(self, didLoad: items)
    }
}

class ViewController: DataSourceDelegate {
    let source = DataSource()

    init() {
        source.delegate = self
    }

    func dataSource(_ source: DataSource, didLoad data: [Item]) { /* ... */ }
}

The AnyObject constraint admits weak. The conventional Swift delegate pattern.

Closure with weak self

class ViewController {
    var loader: DataLoader = DataLoader()

    func loadData() {
        loader.fetch { [weak self] result in
            guard let self else { return }
            self.handleResult(result)
        }
    }
}

The [weak self] admits the controller deallocating while the request is in flight.

Closure with unowned

class Controller {
    let timer: DispatchSourceTimer

    init() {
        timer = DispatchSource.makeTimerSource()
        timer.setEventHandler { [unowned self] in
            self.tick()                            // safe — timer is owned by self; can't outlive
        }
    }
}

The [unowned self] admits no nil-check overhead; conventional when the closure’s lifetime is scoped to self.

Cleanup with deinit

class Subscription {
    private let cancelToken: AnyObject

    init(token: AnyObject) {
        self.cancelToken = token
    }

    deinit {
        // Automatic cleanup
        NotificationCenter.default.removeObserver(cancelToken)
    }
}

Observer pattern

class EventBus {
    private var listeners: [WeakRef<AnyObject>] = []

    func subscribe(_ listener: AnyObject) {
        listeners.append(WeakRef(value: listener))
    }
}

struct WeakRef<T: AnyObject> {
    weak var value: T?
}

The pattern admits substantial observer-pattern implementations without retain cycles.

Cache with weak values

class WeakCache<Key: Hashable, Value: AnyObject> {
    private var cache: [Key: WeakRef<Value>] = [:]

    func get(_ key: Key) -> Value? {
        cache[key]?.value
    }

    func set(_ key: Key, _ value: Value) {
        cache[key] = WeakRef(value: value)
    }
}

The pattern admits caches that don’t prevent value deallocation.

isKnownUniquelyReferenced for COW

struct CowArray<T> {
    private final class Storage {
        var items: [T]
        init(_ items: [T]) { self.items = items }
    }

    private var storage: Storage

    init() { storage = Storage([]) }

    mutating func append(_ item: T) {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage(storage.items)       // copy
        }
        storage.items.append(item)
    }
}

The isKnownUniquelyReferenced admits implementing COW manually.

Avoiding retain cycle in DispatchQueue

class Service {
    func processInBackground() {
        DispatchQueue.global().async { [weak self] in
            self?.process()
        }
    }
}

Combine subscription cleanup

import Combine

class ViewModel {
    private var cancellables: Set<AnyCancellable> = []

    func bind() {
        publisher.sink { [weak self] value in
            self?.update(with: value)
        }
        .store(in: &cancellables)
    }

    deinit {
        // cancellables are automatically cancelled when the set is released
    }
}

A note on the conventional discipline

The contemporary Swift memory advice:

  • Trust ARC — manual memory management is not admitted.
  • Use weak for delegate patterns.
  • Use [weak self] in closures stored on instances.
  • Use unowned only when lifetime is guaranteed.
  • Use final on classes — admits ARC optimisations.
  • Prefer value types — no ARC overhead, no cycles possible.
  • Use deinit for cleanup of non-Swift resources.
  • Profile with Instruments for memory issues.
  • Use Xcode’s Memory Graph Debugger to find cycles.
  • Use AnyObject constraint for class-only protocols admitting weak.

The combination — Automatic Reference Counting, the strong/weak/unowned distinction, capture lists for closures, deinit for cleanup, the conventional discipline of value types as the default — is the substance of Swift’s memory model. The discipline produces deterministic, predictable memory behaviour with substantial protection against the conventional reference-cycle pitfalls.