Mastering Instruments (Part 2.5): malloc, free, and ARC — How Memory Works Under the Hood
Viscerally understand what happens when your code runs. Visualize malloc, free, reference counting, and retain cycles with interactive components.
In Part 2 we talked about the Stack and the Heap. We said that malloc reserves memory, that free releases it, that ARC keeps track of references. But let’s be honest — if you’ve never worked at that level, those words feel abstract. Like reading a car manual in another language.
I’ve been writing Swift for years and for a long time those concepts just flew past me. I knew they existed, but I didn’t feel them. I didn’t viscerally understand what happens when I write let person = Person(name: "Juan"). And that lack of understanding catches up with you eventually — when your app has a memory leak you can’t track down, or when Instruments shows you data you can’t interpret.
This article exists to close that gap. We’re not going to stay in theory — we’re going to see how memory works with interactive visualizations. By the end, you’ll understand malloc, free, refCount, and retain cycles as if you were watching them in slow motion.
The difference between knowing ARC exists and understanding how it works is the difference between driving a car and knowing what to do when it breaks down.
The Stack: Fast Because It’s Simple
Let’s start with the easy part. The Stack is the fastest memory your app has because it works with a brutal rule: last in, first out. When you call a function, a “frame” is created with space for its variables. When the function returns, the frame is destroyed. No negotiation, no searching — just a pointer moving up and down.
Let’s see it in action. In the following visualizer, click each step to see how the Stack grows when we call a function and shrinks when that function returns:
How does the Stack move?
Click each step to see how the stack frame grows and gets destroyed.
Did you see what happened? The entire lifecycle of the function — creating variables, computing, returning — was simply moving a pointer. There was no malloc. No free. No ARC. Just a pointer that advanced and retreated.
That’s what makes the Stack so fast. But it has a huge limitation: it only works with data whose size is known at compile time and that doesn’t need to survive the function that created it. For everything else, we need the Heap.
malloc and free: Checking In and Out of a Memory Hotel
If the Stack is a stack of plates — orderly, predictable — the Heap is a hotel. You have rooms available of different sizes, and you need a system to manage them.
-
malloc(size)is like walking up to the front desk and asking for a room. The system finds a free space of the size you need, marks it as “occupied”, gives you the key (a pointer), and registers you in the system. That takes time — it’s not just moving a pointer like the Stack. -
free(pointer)is checking out. You hand back the key, the system marks the room as “available” again. But here’s the problem: if nobody checks out, the room stays occupied forever. That’s a memory leak.
And there’s another subtler problem: fragmentation. If you reserve and free rooms in random order, you end up with small gaps between the occupied ones. You might have 100 free rooms total, but no contiguous block of 50 — and that makes a reservation of 50 fail even though there’s “enough” space.
Play with the following visualizer. Reserve blocks of different sizes, free some, and watch how the Heap fragments:
malloc and free: the memory hotel
Allocate and free memory blocks. Watch how the Heap fragments.
In Swift, you never call malloc or free directly. The compiler and ARC do it for you. But every time you write let person = Person(name: "Juan") with a class, that’s a malloc under the hood. And every time that variable goes out of scope and nobody else references it, that’s a free.
ARC: The Invisible Counter That Decides Who Lives and Who Dies
Now comes the key question: who decides when to call free? In languages like C, you do it manually — and it’s an infinite source of bugs. In Swift, that responsibility belongs to ARC (Automatic Reference Counting).
The concept is elegant:
- Every object on the Heap has an invisible counter called refCount (reference count)
- When you create a reference to the object (
let ref = person), the refCount goes up: +1 - When that reference disappears (the variable goes out of scope, or you set it to
nil), the refCount goes down: -1 - When the refCount reaches 0 — nobody points to the object anymore — ARC calls
deinitand thenfree
It’s like a counting system at the hotel: as long as at least one guest has a room key, the room stays assigned. When the last guest returns their key, automatic checkout.
Explore the following simulator to see ARC in action. It has three scenarios: the normal ARC cycle, a retain cycle (the most common memory bug in Swift), and the fix with weak:
ARC: the invisible counter
Explore how ARC manages memory, and what happens when a retain cycle forms.
Retain Cycles: When They Happen and How to Prevent Them
Retain cycles aren’t a rare edge case. They appear in extremely common patterns:
The Classic Delegate
class ViewController { var delegate: SomeDelegate? // strong reference}
class SomeDelegate { var viewController: ViewController? // strong reference back = cycle!}The fix: The delegate should always be weak:
class SomeDelegate { weak var viewController: ViewController? // weak = no cycle}Closures Capturing self
class DataLoader { var onComplete: (() -> Void)?
func load() { onComplete = { self.updateUI() // self captured strongly = potential cycle! } }}The fix: Use [weak self] in the capture list:
onComplete = { [weak self] in self?.updateUI() // weak capture = no cycle}- weak → The reference automatically becomes
nilwhen the object is freed. Always safe. Use it when you’re not 100% sure the object will outlive the reference. - unowned → The reference does NOT become
nil. If the object is freed and you access the reference, your app crashes. Use it only when you’re absolutely certain the object will outlive the reference (e.g. a child that always dies before its parent). - Rule of thumb: When in doubt, use
weak. The cost of unwrapping an optional is infinitely less than a crash in production.
Connecting with Instruments
Everything you saw in this article — malloc reserving blocks, free releasing them, refCount going up and down, retain cycles trapping memory — is exactly what Instruments records and shows you visually. Now when you open a trace and see “Allocations” or “Leaks”, you’ll know what that data represents.
But before we dive into memory instruments, we need to talk about methodology. In Part 3 we’ll learn to diagnose performance problems as a scientific process — with hypotheses, experiments, and data. We’ll explore the differences between Time Profiler, CPU Profiler, and Processor Trace, and when to use each one. We’ll also cover advanced techniques like Charge, Prune, and Flatten for manipulating call trees with surgical precision.
Now you understand what happens under the hood. In Part 3 you’ll learn to use that knowledge as a scientific process — not intuition, but data.
References
- Automatic Reference Counting — The Swift Programming Language — Apple’s official reference on ARC, strong/weak/unowned references, and retain cycles.
- Improving App Responsiveness — Apple Documentation — Apple’s guide on how Heap work affects your app’s responsiveness.
Related
-
- swift
- swift-zero-expert
- swift-fundamentals
Swift from Zero to Expert #3: Strings and Characters — much more than text
Unicode scalars, grapheme clusters, why string[0] doesn't exist in Swift, and how Substring shares memory with the original String.
-
- swift
- swift-zero-expert
- swift-fundamentals
Swift from Zero to Expert #2: Collections — Array, Set and Dictionary under the hood
Discover how Swift's collections work inside: when to use each one, their algorithmic complexity, and the elegance of copy-on-write.
-
- swift
- swift-zero-expert
- swift-fundamentals
Swift from Zero to Expert #1: Data types, operators, and how Swift thinks about memory
First article in the Swift from Zero to Expert series. We explore fundamental data types, operators, and why Swift decides to put things on the stack.