Why doesn't Swift use a traditional garbage collector?
\nIf you came to the Swift world from Python or Java, you were probably surprised that this language doesn't have a classic garbage collector (Garbage Collector). Instead, Apple implemented the Automatic Reference Counting (ARC) mechanism. And this is not just a whim: on mobile devices, every processor cycle and every byte of memory counts. A traditional GC "freezes" the application during collection, which would be a disaster for iOS with its requirement for smooth animations (60 FPS).
\nIn this article, we'll break down how memory management actually works in Swift, where the pitfalls with cyclic references lie, and how to write code that doesn't "leak" into nowhere.
\n\nARC: how does it work under the hood?
\nARC is not a garbage collector in the usual sense. It works at compile time: the compiler inserts retain and release instructions in the right places. But there's a nuance: ARC cannot handle cyclic references (retain cycles). If two objects hold strong references to each other, their reference count will never drop to zero — memory will leak.
Example: a simple class and reference counting
\nclass Person {\n let name: String\n init(name: String) {\n self.name = name\n print("\\(name) appeared in memory")\n }\n deinit {\n print("\\(name) disappeared from memory")\n }\n}\n\nvar person1: Person? = Person(name: "Alice") // retain count = 1\nvar person2 = person1 // retain count = 2\nperson1 = nil // retain count = 1\nperson2 = nil // retain count = 0 → deinit\n\nIn the console you will see: "Alice appeared in memory" and after a while — "Alice disappeared from memory". All fair.
\n\nCyclic references: the main enemy of memory
\nImagine: you have a User and an Account. The user has an account, and the account knows its owner. If both properties are strong references, you get a closed loop:
class User {\n let name: String\n var account: Account?\n init(name: String) { self.name = name }\n deinit { print("User \\(name) deleted") }\n}\n\nclass Account {\n let number: String\n var owner: User?\n init(number: String) { self.number = number }\n deinit { print("Account \\(number) deleted") }\n}\n\nvar user: User? = User(name: "Boris")\nvar account: Account? = Account(number: "12345")\nuser?.account = account\naccount?.owner = user\n\nuser = nil // User not deleted! account?.owner still holds a reference\naccount = nil // Account not deleted! user?.account holds a reference\n// Memory leak!\n\nNo deinit messages will appear in the console. The objects are stuck in memory forever. This is a retain cycle — a classic memory management problem in Swift.
How to fix it? Weak and unowned
\nThe solution is to make one of the references weak or unowned. The difference:
- \n
- weak — always optional, automatically becomes
nilwhen the object is deleted. Safe. \n - unowned — not optional, assumes the object lives longer. Accessing a deleted object causes a crash. \n
class Account {\n let number: String\n weak var owner: User? // ← weak reference\n init(number: String) { self.number = number }\n deinit { print("Account \\(number) deleted") }\n}\n// Now user = nil will deallocate User, and account?.owner will become nil\n\n\nGarbage collector vs ARC: which is faster?
\nLet's be honest: ARC is not a "garbage collector" in the classical sense. A real GC (like in Java, Go, or Python) works at runtime: it periodically scans the heap, finds unreachable objects, and frees them. This requires pauses (stop-the-world).
\nARC, on the other hand, is static