Memory Graph: what it is, object graph and detecting cyclic references

Author: IT Sectr Published: 2026-05-07 Reading time: 10 min

Memory Graph is a visual tool in the Xcode Debug Navigator that displays a graph of objects in the application’s memory along with their mutual references. Unlike a heap dump, Memory Graph shows not just a list of objects, but a directed reference graph where each node is an object and each edge is a reference (strong, weak, unowned). According to Apple WWDC 2018, the tool allows you to visually detect retain cycles and memory leaks in seconds, without needing to analyze raw heap dump data.

Key Takeaways

  • Memory Graph is a visual graph of objects in Xcode memory, showing real-time references between objects.
  • Retain cycle is detected by a closed loop in the graph — two or more objects reference each other with strong references.
  • Backtrace for each graph edge shows where and when the reference was established, simplifying the search for the leak source.
  • Filtering by class name and reference type (strong/weak) allows you to quickly isolate problematic objects.
  • Integration with the Memory Report in Xcode allows you to track memory usage changes in real time.

What Is Memory Graph and How It Works

Memory Graph is a component of the Xcode Debug Navigator (introduced in Xcode 10, WWDC 2018) that builds a directed graph of all objects in the memory of the debugged process. Each graph node is a class instance (Objective-C or Swift), each edge is a reference to another object. The edge color indicates the reference type: blue — strong, green — weak, gray — unowned. The graph is built based on LLDB and Objective-C runtime data, so the application must be compiled in Debug configuration with symbols enabled for proper operation.

How it works: when the application is paused at a breakpoint, Xcode requests all live objects and their references from the runtime via LLDB. LLDB uses objc_getClassList and iterates through allocation regions to build the complete graph. On ARM64 (Apple Silicon), additional hardware means are used for allocation tracking without slowdown. Graph construction time depends on heap size: for a typical iOS application (50–200 MB), the graph builds in 1–3 seconds.

According to Apple, Memory Graph is the only tool that can visualize retain cycles without code modification or adding instrumentation. Unlike Instruments Leaks, Memory Graph works in real time inside Xcode and does not require a separate profiler launch. This makes it the first tool of choice for quick memory leak diagnostics during development.

How Memory Graph Differs from a Heap Dump

Heap dump provides a table of all objects with numbers (shallow size, retained size) — it’s optimal for quantitative analysis. Memory Graph provides a visual picture of connections — optimal for finding cyclic references. The tools complement each other: first Memory Graph for quick retain cycle detection, then heap dump via Instruments Allocations for precise retained size measurement. According to objc.io, the combination of both methods covers 95% of memory leak scenarios.

Detecting Retain Cycles with Memory Graph

Retain cycle is a situation when two or more objects hold each other with strong references, forming a closed loop. ARC cannot deallocate such a loop because the retain count of each object never reaches zero. A classic example: ViewController and View, where View has a strong reference to a closure that captures self (ViewController). Memory Graph displays such loops as rings (cycles), highlighting them for quick identification.

When Xcode detects a retain cycle, it highlights it with an orange outline and shows a warning in the Debug Navigator. Clicking on the cycle displays the chain of references forming the closed loop. The developer only needs to determine which strong edge should be weak — usually this is a reference from a child object to the parent (e.g., delegate or closure).

swift
class ViewController: UIViewController {
    let service = DataService()

    override func viewDidLoad() {
        super.viewDidLoad()
        // ❌ Retain cycle: ViewController → service → closure → ViewController
        service.fetchData { self.updateUI($0) }
    }

    func updateUI(_ data: Data) {}
}

class DataService {
    var completion: ((Data) -> Void)?

    func fetchData(handler: @escaping (Data) -> Void) {
        self.completion = handler
    }
}

In Memory Graph you will see a triangle: ViewController → DataService → closure → ViewController. The solution is to make the capture of self weak: [weak self]. After the fix, Memory Graph will show a green edge from the closure to ViewController, and the retain cycle will disappear.

swift
// Fixed code — weak capture of self
service.fetchData { [weak self] data in
    guard let self else { return }
    self.updateUI(data)
}

Memory Graph Debugger Interface in Xcode

The interface of the Memory Graph Debugger consists of three panels: the left — a list of all live objects (grouped by class) with instance counts; the center — a visual graph with draggable nodes; the right — an inspector for the selected object or edge. The object list displays: a class icon, the number of instances in memory, the total retained size, and the percentage of the entire heap. Filtering by class name supports regular expressions.

Navigating the Graph

Graph nodes can be dragged to improve readability. Double-clicking a node opens detailed information about the object: all its properties with types and values, a call stack (backtrace) for each property, and retain/release history. Backtrace is a key feature: it shows which exact line of code established the reference to the object. This allows finding the leak source without manually reviewing all the code.

For complex graphs, Xcode provides automatic layout via Layout → Hierarchical or Cluster. The hierarchical layout places root objects at the top and children below, simplifying chain search. Cluster grouping groups related objects into clusters, which is convenient when the graph contains several isolated groups. According to Apple, for most applications the hierarchical layout is recommended — it is intuitive and takes less time for visual analysis.

lldb
// LLDB commands used by Memory Graph under the hood
(lldb) script import lldb.macosx.heap
(lldb) script heap.find_variable("viewController")
0x600000c4b80: ViewController
(lldb) script heap.refs 0x600000c4b80
0x600000c4b80 -> 0x600003a4c00 (DataService)
    ivar: _service, offset: 16

Graph Analysis: Finding and Fixing Leaks

A systematic approach to Memory Graph analysis includes several stages. Stage 1: run the application, perform a scenario that potentially causes a leak (open/close a screen, make a network request). Stage 2: click the Memory Graph button in Debug Navigator — Xcode builds the graph. Stage 3: check for orange retain cycle warnings in the left panel. Stage 4: for suspicious objects, use the Show only cycles option — only nodes involved in cyclic references will be displayed.

Using Backtrace to Find the Source

Once a retain cycle is found, click on the cycle edge and open the inspector panel. The Backtrace section shows the call stack at the moment this reference was established. For example, if the edge leads from a closure to self, the backtrace will show in which method and on which line of code the closure was created. This eliminates the need to guess — you immediately see the point where the problematic reference was created. According to WWDC Labs, backtrace analysis reduces retain cycle diagnostic time from 15–20 minutes to 2–3 minutes.

swift
class ProfileViewController: UIViewController {
    var profileView: ProfileView!

    override func viewDidLoad() {
        super.viewDidLoad()
        profileView = ProfileView()
        // Memory Graph will show retain cycle here
        profileView.onTap = { [unowned self] in
            // ⚠️ unowned may cause crash when self is nil
            self.navigateToDetail()
        }
    }

    func navigateToDetail() { }
}

// ✅ Correct: [weak self] + guard let self
profileView.onTap = { [weak self] in
    guard let self else { return }
    self.navigateToDetail()
}

Filtering Out Unnecessary Objects

Memory Graph can display thousands of objects, making it difficult to search. Use filters in the left panel: enter a class name (e.g., ProfileViewController) to display only instances of that class. Then select an instance that should have been deallocated (if the screen is closed but the object remains). Apply Show Reachable From — only references relevant to this object will be displayed, hiding the rest of the graph.

Practical Tips for Using Memory Graph

Experienced developers use Memory Graph not only for finding leaks, but also for proactive memory control. Check Memory Graph after every major architectural change — adding a new delegate, closure, or NotificationCenter subscription. Just run a typical scenario and make sure objects are being deallocated correctly and retain cycles are absent. This takes 2–3 minutes but prevents hours of subsequent debugging.

Combination with Memory Report

Memory Report in Xcode (Debug Navigator tab) shows a real-time memory usage graph. Use it together with Memory Graph: open Memory Graph when memory usage spikes. For example, when scrolling a long list with cells loading images, Memory Graph will show which objects are being created and which are being deallocated. If the number of objects grows without decreasing — this is a potential leak visible before it causes a crash. According to Apple, the combination of Memory Graph + Memory Report is the recommended workflow for all iOS developers starting from Xcode 12.

objective-c
// Example of a leak in Objective-C through delegation
@interface DownloadManager : NSObject
@property (strong) id delegate; // ❌ Must be weak!
@end

@implementation DownloadManager
// Memory Graph will show retain cycle:
// ViewController → DownloadManager.delegate → ViewController
@end

// Fix: weak property
@property (weak) id delegate;

Profiling Closures

Pay special attention to closures — the most common source of retain cycles in Swift. When capturing self inside a closure that is stored as a property of an object, a classic cycle is formed. Memory Graph displays this as a closure (a node with the {} symbol) connected by blue edges to captured objects. Regularly check all closures, especially those used in asynchronous calls, GCD, Combine, and SwiftUI. According to Point-Free statistics, 90% of leaks in Swift projects are related to closures that capture self.

Frequently Asked Questions

Does Memory Graph work only for Objective-C or for Swift too?

Memory Graph works for both languages because it uses the Objective-C runtime. Swift objects compatible with ObjC (NSObject subclasses marked with @objc) are fully displayed. Pure Swift structures and classes without ObjC bridging are shown with limitations.

Why doesn’t Memory Graph show some objects?

Objects must be registered in the Objective-C runtime. Swift value types (struct, enum) are not displayed. Make sure the class inherits from NSObject or uses the @objc attribute for visibility in Memory Graph.

How to interpret edge colors in the graph?

Blue — strong reference, retains the object. Green — weak reference, does not affect the lifecycle. Gray — unowned reference. A retain cycle is formed only from blue edges.

Does Memory Graph slow down the application?

Building the graph pauses the application for 1–3 seconds and may temporarily increase Xcode memory consumption by 200–500 MB. The application itself does not slow down since the inspection occurs during a breakpoint pause.

Can Memory Graph be exported for analysis?

Xcode does not support exporting the graph directly. Use a screenshot for documentation or the lldb script heap.find_variable for programmatic data extraction. For detailed analysis, use Instruments Allocations with a heap dump.

Summary

  • Memory Graph is a visual Xcode tool for displaying a graph of objects in memory with their references.
  • Retain cycle is displayed as a closed loop of blue (strong) edges — Xcode highlights it in orange.
  • Backtrace for each graph edge shows the exact location in the code where the problematic reference was created.
  • Filtering by classes and reference types allows isolating leaks in a graph with thousands of objects.
  • Closures are the main source of retain cycles in Swift, Memory Graph displays them as {} nodes.
  • Weak and unowned are solutions for breaking the cycle, but weak is preferred due to safety when nil.
  • Regular Memory Graph checks after architecture changes prevent memory regression in the project.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also