Trace freed/deallocated instances of objects

Originally posted June 2010. This debugging procedure — zombies plus malloc stack history — saved me more than once in the manual-retain/release days, so I documented it thoroughly. The problem it solves is eternal: an object receives a message after being deallocated, and you need to know who allocated it (and who freed it). The tools below have changed, but the workflow maps one-to-one onto what you would do in 2026, which I describe after the original.

The telltale sign in the debugger console looks like this:

-[NSFetchedResultsController class]: message sent to deallocated instance 0x11957d0

Sometimes it says “freed” instead of “deallocated” — same thing. That hex address at the end is the key.

The 2010 procedure (Xcode 3 / GDB era)

To find where the object was allocated, you enabled two environment variables on the executable: NSZombieEnabled YES and MallocStackLoggingNoCompact 1 (in those days: right-click the executable, Get Info, Arguments tab). Then run, reproduce the crash, and ask GDB for the allocation stack:

(gdb) info malloc-history 0x11957d0

And you would get the full allocation backtrace:

Stack - pthread: 0xa0416720 number of frames: 28
 0: 0x9264382d in malloc_zone_calloc
 1: 0x92643782 in calloc
 2: 0x93611618 in _internal_class_createInstanceFromZone
 3: 0x9361ab08 in _internal_class_createInstance
 4: 0x3020275c in +[NSObject allocWithZone:]
 5: 0x3020264a in +[NSObject alloc]
 6: 0x3d92 in -[AMRAPViewController fetchedResultsController] at CrossFit Timer/Classes/AMRAPViewController.m:146
 7: 0x3a18 in -[AMRAPViewController viewWillAppear:] at CrossFit Timer/Classes/AMRAPViewController.m:96
 8: 0x3097c945 in -[UINavigationController _startTransition:fromViewController:toViewController:]
 9: 0x30977c33 in -[UINavigationController _startDeferredTransitionIfNeeded]
10: 0x3097d01e in -[UINavigationController pushViewController:transition:forceImmediate:]

Frame 6 (below the alloc machinery) showed exactly where the doomed object was created — which told you which over-release or premature release to hunt down.

The 2026 procedure

Manual retain/release is gone (ARC has been the default since 2011), and GDB is gone too — but over-releases still happen through bridging, unsafe unowned references, and C interop. The same two-stage workflow survives:

1. Zombies: instead of the NSZombieEnabled environment variable, use the Zombies instrument in Instruments, or edit the scheme and check Diagnostics → Zombie Objects. Deallocated objects become “zombies” that log instead of vanishing, so the message-to-deallocated-instance crash becomes a diagnosable event.

2. Allocation history: instead of info malloc-history, use the Allocations instrument with Record reference counts and Detect VM allocations enabled, or tick Diagnostics → Malloc Stack Logging in the scheme. Then click the zombie’s address in the console and Xcode jumps straight to the allocation and release stack traces — what used to require pasting addresses into GDB is now a click in the memory graph. Apple’s archived malloc debugging documentation explains the underlying machinery, and TN2124 is still the classic deep-dive into what the zombies actually do.

3. The Memory Graph Debugger (Xcode 8+): when you want to see who still references a suspicious object, pause the app and open Debug → Debug Memory Graph. It draws the live object graph — the fastest way I know to find the retain cycle or the stray strong reference behind an over-release.

Same warnings as always

Zombies and malloc stack logging are debugging aids, not optimizations — they make every allocation hold its stack trace and never truly free memory. Never ship a build with them enabled; profile with them, release without them. (In practice: leave them off in the Release scheme and check the archive configuration before you ship, exactly as you had to in 2010.)

References: Apple QA1788, Building with Zombie mode enabled is not allowed in the App Store; Finding memory leaks with Instruments; Instruments tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *