ARC: what it is, how Automatic Reference Counting works in iOS

Author: IT Sectr Published: 2026-03-29 Reading time: 8 min

Automatic Reference Counting (ARC) is a memory management system in Swift and Objective-C that automatically counts the number of references to each object and deallocates it when the count reaches zero. According to Apple Swift Documentation, 2026, ARC is embedded in the compiler and works at compile time, inserting retain/release calls in the right places. Unlike Garbage Collection, ARC does not require a separate collector thread and does not create pauses during application execution.

Key Takeaways

  • ARC — Automatic Reference Counting, a compiler-based memory management system in Swift and Objective-C
  • How it works — each object has a reference counter (retain count); when it reaches zero, the object is immediately deallocated
  • Qualifiers — strong, weak and unowned determine how a reference affects the counter and the object's lifecycle
  • Difference from GC — ARC works deterministically at compile time, without Stop-The-World pauses or a background collector thread
  • Retain Cycle — the main problem of ARC: if two objects reference each other through strong references, their counter never reaches zero

What is ARC?

ARC (Automatic Reference Counting) is a compiler-based memory management mechanism introduced by Apple in Xcode 4.2 (2011) for Objective-C and inherited by Swift. Unlike manual memory management (Manual Retain-Release, MRR), ARC fully automates retain, release and autorelease calls, inserting them at compile time without developer intervention.

ARC is not a garbage collector. It is static analysis with dynamic code insertion: the compiler analyzes object lifetimes and places retain/release at points where objects are created, copied, or go out of scope. The result is deterministic memory deallocation: the object is removed exactly when no more references point to it, without delays or pauses.

According to WWDC 2011 Session 323, switching from MRR to ARC reduced memory-related crash bugs by 70% in Apple applications. Developers stopped manually balancing retain/release, eliminating an entire class of leaks and double-free errors.

How Automatic Reference Counting works

Each object in memory has a reference counter (retain count). When an object is created, the counter is set to 1. When a new strong reference points to the object — the counter increases (retain). When a strong reference disappears — the counter decreases (release). When it reaches zero, the object is immediately deallocated.

The Swift compiler inserts retain/release not on every assignment — it uses static analysis for optimization. For example, if an object is guaranteed not to be used after being passed, the compiler may skip an unnecessary release/retain. This optimization is called ARC Optimization.

swift
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) initialized (retain count: 1)")
    }
    deinit {
        print("\(name) deallocated")
    }
}

func testARC() {
    let p = Person(name: "Alice")  // retain count = 1
    let q = p                      // retain count = 2
    // q goes out of scope
    // retain count = 1
    // p goes out of scope
    // retain count = 0 → deinit
}

This example shows how ARC manages the counter: when q = p is assigned, the counter increases; when q goes out of scope, it decreases. When the last strong reference disappears, the deinitializer is called immediately. No garbage collector waits — memory is freed right away.

ARC vs Garbage Collection: key differences

ARC and Garbage Collection solve the same problem — automatic memory management — but with fundamentally different approaches. The choice between them defines the language architecture: Swift (ARC) vs Java/Go (GC). Let's look at the main differences.

CharacteristicARC (Swift/ObjC)GC (Java/Go)
Deallocation timingDeterministic: immediately when counter reaches zeroNon-deterministic: at the next collection cycle
Execution pausesNone (retain/release inserted at compile time)Stop-The-World pauses (2–200 ms)
OverheadCounter increment/decrement on each referenceObject graph traversal, marking, sweeping
ProblemsRetain Cycle (manual resolution)Heap fragmentation, leaks from forgotten references
Additional threadNot requiredGarbage collector thread required

The key trade-off: ARC provides predictable object lifetimes and zero pauses, but requires the developer to understand retain cycles and choose weak/unowned correctly. GC frees the developer from these concerns, but at the cost of non-deterministic pauses and an additional thread.

Strong, Weak and Unowned: reference qualifiers in ARC

ARC defines three types of reference qualifiers, each affecting the counter and object lifecycle differently. Choosing the right qualifier is the foundation of safe memory management in Swift.

Strong

Strong is the default qualifier. Each strong reference increases the object's retain count by 1. As long as at least one strong reference exists, the object stays alive. All class properties and local variables in Swift are strong by default. Strong references create an ownership relationship: object A owns object B.

Weak

Weak is a reference that does not increase the retain count. An object can be deallocated even if a weak reference points to it. After deallocation, the weak reference is automatically set to nil. Weak references are always declared as var with an optional type (?). They are used to break retain cycles, especially in the delegate pattern.

Unowned

Unowned is a non-owning reference that, like weak, does not increase the retain count. However, an unowned reference is not set to nil after deallocation — accessing a deallocated object causes a crash. Unowned is used when it is guaranteed that the object lives at least as long as the referencing object. Typical use cases are closures and parent-child relationships with guaranteed lifetime.

swift
class Customer {
    let name: String
    var card: CreditCard?         // strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

class CreditCard {
    let number: String
    unowned let customer: Customer   // unowned — does not own
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card \(number) deallocated") }
}

var customer: Customer? = Customer(name: "Bob")
customer?.card = CreditCard(number: "1234", customer: customer!)
customer = nil
// Customer and CreditCard both deallocated — no retain cycle

Here CreditCard uses an unowned reference to Customer. Customer owns the card (strong), and the card does not own the customer (unowned). When Customer is deallocated, both objects are freed — no retain cycle occurs. If card.customer were strong, the cycle would block deallocation.

Common ARC problems and their solutions

Despite automation, ARC is not a silver bullet. Developers encounter several typical problems that require understanding of the internal memory management mechanism.

Retain Cycle in Closures

Closures in Swift capture external variables by strong reference. If a closure is assigned to a class property and captures self — a retain cycle occurs: the class holds the closure, the closure holds self. The solution is a capture list with weak or unowned.

swift
class NetworkManager {
    var completionHandler: ((Data?) -> Void)?
    var data: Data?

    func fetchData() {
        completionHandler = { [weak self] result in
            guard let self else { return }
            self.data = result
            self.processResult()
        }
    }

    func processResult() { }
}

The capture list [weak self] creates a weak reference to self inside the closure. This breaks the potential retain cycle. Guard let self ensures the object is alive before executing the code. weak self is the standard practice for asynchronous closures in Swift.

Retain/Release Performance

Although retain/release are lightweight operations, in hot loops frequent counter increments/decrements introduce overhead. In Swift 5.9+, the compiler uses optimization that removes redundant retain/release if the analyzer proves it safe. However, in Objective-C, retain/release can still be a bottleneck in high-load scenarios with millions of calls per second.

Autorelease Pool

Autorelease Pool is a deferred release mechanism used in Objective-C and some Swift scenarios. Objects are placed into the pool and receive release when the pool is drained. In loops with many temporary objects (e.g., JSON parsing), creating a custom autoreleasepool reduces peak memory consumption.

Frequently Asked Questions

How does ARC differ from manual memory management (MRR)?

In manual memory management (MRR), the developer explicitly called retain, release and autorelease. ARC inserts these calls automatically at compile time, eliminating the risk of double-free, leaks from forgotten release, and retain/release balancing errors.

Can ARC work with C/C++ code?

ARC manages only Objective-C objects and Swift classes. For C/C++ structures and pointers, ARC does not apply — these objects are managed manually or via C++ smart pointers (shared_ptr, unique_ptr). Core Foundation objects (CFString, CGColor) are also not covered by ARC.

When to use weak and when unowned?

weak — when the object may be deallocated before the referencing object (delegates, asynchronous closures). unowned — when the object is guaranteed to live at least as long as the referencing object (parent-child where the child cannot exist without the parent). If unsure, choose weak.

What are existential types and how do they affect ARC?

Existential types (protocol as type) in Swift wrap the value in a special container (existential container). This increases the number of retain/release at protocol boundaries. In Swift 5.7+, opaque result types and some parameters reduce overhead by eliminating the container.

How to check retain count in Swift?

There is no direct API for reading retain count in Swift — it is considered an implementation detail. For diagnostics, use Instruments (Allocations, Leaks) or the Memory Debugger in Xcode. These tools show the number of live class instances and retention chains.

Summary

  • ARC — a compiler-based memory management system for Swift and Objective-C that works via reference counting
  • Principle — each object has a retain count; when it reaches zero, the object is freed immediately and deterministically
  • Difference from GC — ARC works without a background thread or Stop-The-World pauses, but requires controlling retain cycles
  • Strong — increases the counter; weak and unowned do not, but unowned does not nil out on deallocation
  • Closures — the main cause of retain cycles in Swift; [weak self] capture list is the standard solution
  • Autorelease Pool — a deferred release mechanism for temporary objects in loops and custom scenarios
  • Diagnostics — Xcode Memory Debugger, Instruments and LeakCanary (via ObjC bridge) for finding issues

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