Dive into Swift's Memory Management

Swift uses ARC to track and deallocate unused objects. Learn about the three types of reference counts and how ARC works — in this detailed post.

Alex Dremov
Dive into Swift's Memory Management

In this post, I'll explore how Swift's memory management works under the hood, and how the memory modifiers: unowned and weak, affect an object's lifetime. You'll get a deeper understanding of how Swift manages objects' lifetime internally.

💥
Swift memory management is one of the basic interview questions. It was asked in every iOS developer interview I've ever been to

Memory Management

For example, in C, only the developer is in charge of deallocating unused objects. This can lead to memory leaks, double deallocations, or the use of invalid memory areas.

We don't want this.

Swift uses automatic reference counting (ARC) under the hood to deduce objects' lifetime and automatically deallocate unused objects. Swift has three different types of the reference count. They count how many other instances use an object. And when it is not needed, it is deallocated.

💡
This guide will progress from a general overview to the internals of ARC. Even if you're familiar with Swift's memory management, there's a high chance that you will learn something new

Strong Reference

The counter that is responsible for deallocation is a strong reference counter (RC). The strong RC counts strong references to the object. When the strong RC reaches zero the object is deinited.

A strong reference is just a regular object usage. Creating a variable, or a constant, or saving a reference to an object in another object's property — they all create a strong reference.

Why a developer should even care about reference counting? Seems like a low-level implementation detail that is not important. But actually, it's crucial.

Take a look at this example

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")

john!.apartment = unit4A // Person -> Apartment: strong reference
unit4A!.tenant = john // Apartment -> Person: strong reference

john = nil // Person is no longer needed
unit4A = nil // Apartment is no longer needed

In the above example, the Person and Apartment objects have a strong reference to each other, creating a retain cycle. As a result, when you set both john and unit4A to nil, the deinitializers are not called and the objects are not deallocated.

Retain cycle image
💥
This situation is called a memory leak. In Swift, it occurs only in the case of a retain cycle. Two objects depend on each other and they will never be deallocated.

That's where memory management modifiers come in handy.

Weak Reference

One of the solutions to the problem of a retain cycle is a weak reference. It is created using the weak modifier like that:

let person = Person(name: "John Appleseed") // person is a strong reference
weak var weakPerson = person // weak reference to the same object

Weak var always has an optional type and cannot be constant (let). That's because the object can be deallocated while it is still referenced by a weak variable. In this case, the variable is automatically set to nil.

💡
Consider weak reference like the one that needs an object but can go on correctly without it (using nil), allowing it to deallocate when nobody else needs it

Let's take a look at the solution to the problem above using the weak modifier:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    
    weak var tenant: Person?
    
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")

john!.apartment = unit4A // Person -> Apartment: strong reference
unit4A!.tenant = john // Apartment -> Person: weak reference

john = nil
unit4A = nil
Strong and weak reference image

Now, retain cycle is no longer here. At first, the Person object is deallocated because it has no strong references to it. Then, the Apartment object is deallocated.

No memory leak!

That's it. That is how you break retention cycles in Swift. There is one more modifier that can help you with that.

Unowned Reference

An unowned reference is very similar to a weak reference cause it also does not increase a strong reference count. The difference is that it's up to the developer to not use an invalid object.

Unowned variables can be constant or non-optional. When an object is deallocated, ARC does not set the unowned reference’s value to nil. However, if you try to access a deallocated object, you will catch a runtime error.

😡
Use an unowned reference only when you are sure that the reference always refers to an instance that has not been deallocated

Here's a similar example:

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

var john: Customer? = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

john = nil // No retain cycle, both objects are deallocated
Subscribe and don't miss posts!

Three Reference Counters

So, how does all this magic works inside? Swift sources have an amazing detailed description of all processes under the hood.

The strong RC counts strong references to the object. When the strong RC reaches zero the object is deinited, unowned reference reads become errors, and weak reference reads become nil. The strong RC is stored as an extra count: when the physical field is 0 the logical value is 1.

The unowned RC counts unowned references to the object. The unowned RC also has an extra +1 on behalf of the strong references; this +1 is decremented after deinit completes. When the unowned RC reaches zero the object's allocation is freed.

The weak RC counts weak references to the object. The weak RC also has an extra +1 on behalf of the unowned references; this +1 is decremented after the object's allocation is freed. When the weak RC reaches zero the object's side table entry is freed.

But what is a side table and why is it needed?

💥
What's side table is another popular interview question, usually more advanced

Side Table

An object conceptually has three refcounts. These refcounts are stored either "inline" or in a "side table entry" pointed to by the internal field. You cannot access these fields from Swift directly

class User {
	var id: Int
    var name: String
    
    init(id: Int, name: String) {
    	self.id = id
        self.name = name
    }
}

let user = User(id: 0, name: "John")
💡
Remember that unowned has +1 on behalf of strong reference and weak has +1 on behalf of unowned references

Objects initially start with no side table. They can gain a side table when a weak reference is formed.

Gaining a side table entry is a one-way operation; an object with a side table entry never loses it. This prevents some thread races.

weak var weakUser = user // Side table implicitly created
A side table is created
A side table is created

Strong and unowned variables point at the object. Weak variables point at the object's side table.

This idea is fundamental to understanding how weak references work. By pointing not to the object but to the side table, the object itself can be deinitialized and fully deallocated.

Weak and Unowned. Deep Differences

Now, by looking at the implementation we can notice important differences between weak and unowned.

Performance

Using unowned introduces less overhead than using weak. That's because weak variables reference the object through a side table. This means that there's one more pointer hop to reach the object.

Unowned references point directly to the object, so they do not have such overhead.

Deallocation vs deinitialization

According to the sources, when the strong RC reaches zero the object is deinited. And when the unowned RC reaches zero the object's allocation is freed.

That means that object memory is not available for realocation until all unowned references disappear.

💥
If an object holds a large amount of memory, its memory will not be available until the last unowned reference disappear.

If lack of memory is a problem, consider using weak reference because it allows objects to be fully deallocated even when there are alive weak references.

Common Problems

The example of Person and Apartment retain cycle can be trivial. It's important to know about common cases when retain cycle appears.

Closures, strong capture, and self

By default, a closure expression captures constants and variables from its surrounding scope with strong references to those values.

As we've already noted, uncontrollable strong references may create a retain cycle. An escaping closure that refers to self needs special consideration if self refers to an instance of a class. Capturing self in an escaping closure makes it easy to accidentally create a strong reference cycle.

For example:

class Person {
  var name: String
  var voice: Voice? = nil

  init(name: String) {
    self.name = name
    self.voice = Voice {
      print("I'm \(self.name)")
    }
  }
  func say() { voice?.say() }
  deinit {
    print("Person deallocated")
  }
}

class Voice {
  var say: () -> ()
  init(say: @escaping () -> ()) { self.say = say }
  deinit {
    print("Voice deallocated")
  }
}

var person: Person? = Person(name: "Alex")
person!.say()

person = nil

Which outputs only this line — without deinit prints

My name is Alex

What's going on here? Let's draw a strong references graph:

Retain cycle with closure
Retain cycle with closure

And, as expected, there is a pretty notable strong reference cycle. The problem is in  the creation of the Voice instance:

self.voice = Voice {
	print("My name is \(self.name)")
}

Here, self is captured with a strong reference to the escaping closure. To solve that, we can capture self with the weak modifier:

self.voice = Voice {[weak self] in
	guard let self = self else { return; }
	print("My name is \(self.name)")
}

With such modification, we receive an expected output:

My name is Alex
Person deallocated
Voice deallocated
💥
Do not use weak self when it is not needed. Remember that strong reference is required so that object is not deallocated before it is needed.

Final notes

If you want to achieve an even deeper understanding of ARC internals, definitely check the ARC source code. You can start with this amazing description of an object's lifetime state machine.

swift/RefCount.h at 3bac57d9ac20eb9a6e41fd3c32e8d6fb23e37a47 · apple/swift
The Swift Programming Language. Contribute to apple/swift development by creating an account on GitHub.

Hope that this post was helpful to you. Feel free to leave a comment or to reach me through my social nets!

References

swift/RefCount.h at main · apple/swift
The Swift Programming Language. Contribute to apple/swift development by creating an account on GitHub.
Memory Management in Swift: Understanding Strong, Weak and Unowned References
Behind all the coding that we are doing, you probably have noticed some of your variables with the reference of strong, weak or unowned…
Automatic Reference Counting — The Swift Programming Language (Swift 5.7)
Expressions — The Swift Programming Language (Swift 5.7)

Share

Subscribe to Alex Dremov

Get the email newsletter and receive valuable tips to bump up your professional skills