Skip to content
SwiftMind

Memory Management

4 interview questions on memory management.

Explain how ARC decides when to free an object, and where that logic falls short.

MidTap to think, 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

Follow-ups

How would you find a leak like that in a running app?

Instruments' Leaks and Allocations instruments, and the memory graph debugger in Xcode, which draws the actual object graph and marks cycles — usually the fastest way to see the offending edge. For a quick check during development, a print or os_log in deinit on suspect types tells you immediately whether a screen is being released when you pop it. The pattern to watch for is memory that grows every time you push and pop the same screen and never comes back down.

Read the concept: Automatic Reference Counting

Walk me through choosing between `weak` and `unowned` for a specific relationship.

SeniorTap to think, then reveal

I ask one question: can I state an invariant that guarantees the referenced object outlives this reference, and is that invariant enforced by code I control?

If yes — the classic case being a child object created by its parent, stored only by that parent, and unreachable once the parent is gone — unowned is honest. It documents the guarantee in the type, avoids an optional that would always succeed, and if I've got the invariant wrong the trap tells me loudly and immediately rather than hiding.

If the answer is no, weak. And "no" covers more than people expect: anything whose lifetime the system controls, anything reachable from a collection someone else mutates, anything torn down asynchronously. A delegate is the standard example — the delegating object has no idea when its delegate goes away, so weak var delegate is the only defensible choice.

The asymmetry that decides ties: an unnecessary weak costs an optional unwrap and a small read overhead. An incorrect unowned costs a crash on a user's device, in a scenario you didn't reproduce. Those aren't comparable, so I default to weak under uncertainty and treat unowned as something I have to justify.

What an interviewer listens for

  • Frames it as an invariant you can state and enforce, not a preference
  • Gives the parent-owns-child case as the legitimate unowned shape
  • Identifies delegates and system-owned lifetimes as weak territory
  • Reasons explicitly about the asymmetric cost of being wrong

Follow-ups

Is `unowned` ever preferable for reasons other than performance?

Yes — it's a stronger piece of documentation. unowned says "this relationship is structural, this object cannot outlive that one", and the compiler and runtime hold you to it. With weak every use site has to handle nil, which means readers can't tell whether nil is a real expected state or defensive noise. When the guarantee genuinely holds, unowned communicates it better.

What about `[unowned self]` in a closure stored on self?

That's the textbook fit — the closure is owned by self, so it cannot possibly outlive it. It's still worth checking nobody copies the closure out to somewhere with a longer life, because that's how the invariant quietly breaks later: the original author was right, and a refactor six months on moved the closure into a queue.

Read the concept: Weak vs Unowned

Explain a retain cycle, and how you'd detect one in a production application.

SeniorTap to think, then reveal

A retain cycle is a loop of strong references. Because ARC frees an object only when its own count hits zero, and every object in the loop is being retained by another member of the loop, the whole group stays alive after it becomes unreachable. It's a leak with no crash and no error.

For detection I'd work in three layers.

During development, a deinit log on view controllers and view models. Push a screen, pop it, watch for the line. It costs nothing and catches the majority of cycles the day they're introduced, which is when they're cheapest to fix.

When something's suspected, Xcode's memory graph debugger. It renders the actual object graph and marks cycles directly, so you see which reference to weaken rather than guessing. Instruments' Allocations with a repeated push/pop is the companion — if the persistent-object count for a type climbs by one per visit and never comes down, that's your leak, and the generation summary tells you what's accumulating.

In production, you don't see individual cycles — you see the aggregate. Memory-related terminations in MetricKit, and jetsam events in the organizer, tell you memory is the problem and which build regressed. Then you reproduce with the tools above. It's also worth logging peak footprint per session so a regression shows up as a trend rather than as user complaints.

Prevention beats detection though: ownership pointing one way, weak delegates, [weak self] in stored closures, and reviewing subscriptions for what they capture.

What an interviewer listens for

  • Correctly explains why counting can't detect a cycle
  • Gives a layered answer: dev-time, debugging, production telemetry
  • Names concrete tools — memory graph debugger, Allocations, MetricKit
  • Mentions the deinit-log technique as the cheap first line
  • Ends on prevention, not just detection

Follow-ups

What's the most common cycle you see in SwiftUI or Combine code?

A subscription that captures self strongly and is stored in a Set<AnyCancellable> on that same object. The object owns the cancellable, the cancellable's closure owns the object. sink { [weak self] in ... } is the fix. It's easy to miss because the cycle runs through a closure and a framework type rather than through two of your own properties, so it doesn't look like the textbook diagram.

Does a `Timer` cause this?

A repeating Timer scheduled with a target retains its target, and the run loop retains the timer, so a view controller that schedules one and never invalidates it is kept alive by the run loop indefinitely. Crucially deinit never runs, so you can't invalidate it there — that's the trap. Either invalidate in viewDidDisappear, or use the block-based API with [weak self].

Read the concept: Retain Cycles

How do you reason about closure captures when reviewing code?

StaffTap to think, then reveal

I read every stored or escaping closure as a lifetime statement and ask two things: what does this keep alive, and what happens if the thing it references is gone when it runs?

The first question finds cycles. If the closure is reachable from the object it captures — stored on it, held in a cancellable set on it, registered with something the object owns — that's a loop, and the capture needs weakening.

The second question is the one reviewers skip, and it finds the subtler bug. Once you weaken the capture, you've decided that if the object dies the work doesn't happen. For a UI update that's correct and desirable. For "flush the cache", "release the lock", "resume the continuation" or "call the completion handler" it's a silent failure with no crash and nothing in the logs, which is much harder to diagnose than the leak it replaced. A continuation that never resumes is the worst version — the awaiting task hangs forever.

So the review outcome isn't always "add [weak self]". Sometimes it's "this closure should capture strongly and this object should live until the work finishes, and the reason belongs in a comment". Sometimes the best answer is to capture the two values it actually needs instead of the whole object, which makes both questions disappear.

I also look for whether the closure needs self at all — a surprising number capture it for one property that could have been snapshotted.

What an interviewer listens for

  • Reads captures as lifetime decisions rather than pattern-matching on syntax
  • Asks both what's kept alive and what happens when it's gone
  • Names silent no-ops — especially unresumed continuations — as the cost of weak capture
  • Suggests value capture as a way to remove the question entirely
  • Concludes that weak self is not the universal answer

Follow-ups

How does structured concurrency change this?

A lot, and mostly for the better. A Task inside an object still captures self strongly by default, but its lifetime is bounded and cancellable rather than open-ended, and async let and task groups are scoped so the capture provably ends with the scope. The remaining hazard is a long-lived Task stored as a property, or an unbounded for await loop over a stream — those are the same cycle in new clothes, and the fix is still to weaken the capture or cancel the task in deinit.

Read the concept: Closure Capture

Drill interview-level questions

Advanced and senior quiz questions only, across every category.

Start