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) 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.
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.
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 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.
| Characteristic | ARC (Swift/ObjC) | GC (Java/Go) |
|---|---|---|
| Deallocation timing | Deterministic: immediately when counter reaches zero | Non-deterministic: at the next collection cycle |
| Execution pauses | None (retain/release inserted at compile time) | Stop-The-World pauses (2–200 ms) |
| Overhead | Counter increment/decrement on each reference | Object graph traversal, marking, sweeping |
| Problems | Retain Cycle (manual resolution) | Heap fragmentation, leaks from forgotten references |
| Additional thread | Not required | Garbage 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.
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 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 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 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.
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.
Despite automation, ARC is not a silver bullet. Developers encounter several typical problems that require understanding of the internal memory management mechanism.
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.
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.
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 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
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.
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.
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.
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.
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
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.
Read also