What are zombie objects and malloc stack logging, and how do you debug dangling references and over-released memory?
Predict first
A manually released Objective-C object is messaged afterwards. With Zombie Objects enabled, what does the runtime report?
// === z.m ===
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
NSObject *o = [[NSObject alloc] init];
NSObject *dangling = o;
[o release];
NSLog(@"%@", [dangling description]);
}
return 0;
}
- “message sent to deallocated instance”, naming the class and selector
- Nothing, the message is ignored
- A stack overflow
Mental model. A zombie is a cardboard cut-out left where a person used to stand. When someone talks to it, it says “I am gone, and you were trying to say this”, which is the clue you were missing.
Swift’s ARC prevents most lifetime bugs, but a dangling reference can still exist: an Objective-C object over-released through manual retain-release or a bad bridge, an unowned or unsafe reference that outlives its target, a C pointer, or a delegate that is assign rather than weak in older code. The crash then happens later, in unrelated code, often as EXC_BAD_ACCESS with a useless stack. Zombie Objects (Scheme ▸ Run ▸ Diagnostics) turns that into a precise message: instead of freeing memory, deallocated objects are replaced by a zombie that, when messaged, reports “message sent to deallocated instance 0x…” with the class and the selector, as the check below shows with a manual over-release. That tells you what was messaged after death; to learn who freed it, enable Malloc Stack Logging and use the memory graph (Q159) or malloc_history to see the allocation and release stacks for that address.
Pick the tool by symptom. Crashes with garbage addresses or EXC_BAD_ACCESS on a freed object: Zombies, then Address Sanitizer (see the sanitizers question) for heap corruption and use-after-free in C and unsafe Swift. A Swift unowned reference whose object is gone is not a zombie: Swift traps with “attempted to read an unowned reference but object was already deallocated”, which is the clue to change it to weak or fix the lifetime. A steadily growing footprint is a leak or retain cycle (Q159, Q160), not a dangling pointer. Note that zombies keep every deallocated object alive, so memory grows during the run and the setting changes behaviour; turn it off for normal use and never ship it.
With zombies on, the freed object is replaced by a placeholder that reports exactly what was sent to it.
Common traps
- Leaving Zombie Objects on. Freed objects are kept, memory grows, and timings change.
- Treating a Swift
unownedtrap as a zombie. It is a different, deliberate trap; the fix is the reference kind or the ownership.
They might follow up with
- Zombies say what; how do you find who? Malloc Stack Logging with the memory graph or
malloc_historyshows the allocation and free stacks. - Why does ARC code still crash with bad access? Unsafe pointers, unmanaged references, ObjC bridging mistakes and C libraries bypass ARC.
Related questions
- 23.3.1 What do the Address, Thread and Undefined Behavior sanitizers catch, what do they cost, and when do you use…
- 23.3.2 Why do some bugs appear only in Release builds, how do assert, precondition and fatalError differ, and how do…
- 3.7.2 How do you find a retain cycle with the Memory Graph debugger?
- 3.7.3 How do you use Instruments’ Allocations, Leaks and VM Tracker to track a memory problem?
← How do you debug layout and views with the view debugger, and how do…