Memory Management With Swift: ARC, Strong, Weak, and Unowned Explained
Tags: swift, ios
Published: 2023-09-11 14:42:27

## Introduction

Memory management is a key part of any programming language. Numerous articles provide a fundamental explanation of how memory management operates, this particular article delves into a more in-depth exploration of the topic.

In this article we consider:

- What is the memory management?
- What is automatic reference counting?
- How `strong`, `weak`, and `unowned` references are implemented?
- What is the life cycle of an object?
- What are Side Tables?

## Defining Memory Management

In simply words, memory is a collection of bytes, where each byte has own address. If we talk in terms of programming then, we have to deal with address space. An address space can consist of following components:

- **text** is a segment that contains an executable instructions.
- **data** is a portion of the virtual address space of a program, which contains the global and static variables that are initialised by the developer.
- **stack** is used for storing local variables.
- **heap** is used for storing dynamically allocated objects.

**Memory management** is the process of controlling and coordinating a computer's main memory. It's critical to understand how it works, otherwise it may lead to random crashes and increased memory consumption.

> In some cases classes may be stored on the stack, and struct may be stored on the heap.

> In Swift 5.6, the `any` keyword was introduced. If you use the `any` keyword, it signifies that the object will be enclosed within an existential container. If the object can fit within three machine words, it will be stored inside the existential container; otherwise, it will be allocated on the `Heap`, and the existential container will only store a reference to the object.

> The Swift compiler may allocate reference types on the stack when their size is fixed, or their lifetime can be predicted. This is called "Stack Promotion." This optimization occurs during the SIL generation phase.

> If a value type is a part of a reference type, it saves on the `Heap`.

## Defining ARC

Memory management in Swift is very tightly coupled with automatic reference cycle.

> Automatic Reference Counting (ARC) is a memory management attribute used to monitor and manage an application's memory usage.

Reference counting applies only to instances of classes. Structures and enumerations are value types, not reference types, and aren’t stored and passed by reference.

Every time when we create a new class instance, ARC allocates a chunk of memory, which contains an information about the created object along with a type of the instance and values that stored in this object.

To make sure that the instances don't be deallocated while they are still needed, ARC tracks how many variables, constants, properties point out to this instance. The object won't be deallocated as long as least there is one reference exists.

## Understanding Strong, Weak, and Unowned

If we are working with structs or enums, ARC isn't managing the memory of those types and we don't need to worry about specifying `weak` or `unowned` for those constants or variables. 

When we have to deal with classes, we can potentially run into a retain cycle. A retain cycle is a situation in which two objects keep strong references to each other indefinitely. To solve this kind of problem we can using `weak` or `unowned` keywords.

- **strong** keeps a firm hold on the instance and doesn't allow it to be deallocated for as long as that strong reference exists.

- **weak** doesn't keep a strong hold on the instance it refers to. A weak reference returns `nil`, when an object it points to is no longer alive.

- **unowned** as a weak one doesn't keep a strong hold. Access to unowned references with a zero counter leads to a crash.

Swift conceptually have several counters for strong, weak, and unowned references. These counters are stored either along with an object right after isa pointer or in a side table, which be explained a lit bit later.

## Defining Swift Runtime

Swift Runtime represents every dynamically allocated object with `HeapObject` struct. It encompasses all the data components that constitute an object in Swift, including reference counts and type metadata.

Within its internal structure, each Swift object possesses three reference counts, corresponding to distinct types of references. During the SIL (Swift Intermediate Language) generation phase, the swiftc compiler strategically incorporates invocations to the `swift_retain()` and `swift_release()` methods. This integration occurs at points deemed suitable, achieved by capturing the initialization and destruction instances of `HeapObjects`.

## Introducing Side Tables

Side Tables are mechanism for implementing `weak` references. 

Side Tables was introduced in Swift 4. Let's dive in a little bit history and take a closer look how it worked before Swift  4.

### Before Swift 4

Let's imagine that we have a class like this:

```swift
class Object {
    let id: Int
    let name: String
}
```

The next picture represents an object into memory:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r1rs06t4fucnrvdwut43.jpg)

The class properties and reference counters are stored in a single object. It helps to get a quicker access that storing it at the other chunk of memory.

After creating a weak reference to an object, the reference count for the weak reference is incremented. Now, consider a scenario where there are no remaining strong references to the object. In such a situation, the object is essentially labeled as a `zombie`. It remains in memory until another object attempts to access it through the weak reference. This means that the zombie object could occupy memory for an extended period.

Another significant issue here: the process of loading an object through a weak link lacked thread safety.

```swift
class Object {}

class WeakHolder {
   weak var weak: Object?
}

for i in 0..<1000000 {
   let holder = WeakHolder()
   holder.weak = Object()
   dispatch_async(dispatch_get_global_queue(0, 0), {
       let _ = holder.weak
   })
   dispatch_async(dispatch_get_global_queue(0, 0), {
       let _ = holder.weak
   })
}
```

This piece of code may potentially result in a crash. Two threads have the potential to simultaneously access an object through a `weak` reference. Before acquiring the object, both threads verify whether the object is marked as a zombie. If both threads receive a positive response, they will both attempt to decrement the count and release memory. One of them will succeed in doing so, while the second thread will trigger a crash by attempting to free memory that has already been deallocated.

### Understanding Swift Tables

The Side Table is a separate chunk of memory which store additional objects information. It's optional, meaning that the object may have a side table, or it may not. Objects which have week pointers to itself can incur the extra cost, and objects which don't need it don't pay for it.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wljcymxzfxhtc1z56ebn.png)

Side Table is created only one the following things happen:

- Weak reference to the object is created
- `strong` and `unowned` counter overflows 

Weak references now point directly to the Side Table, whereas strong and unowned references ones point directly to the object. In this case the object can be fully deallocated.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w50g50xrggrhc21tt0rb.png)

Utilizing `unowned` carries a lower overhead compared to employing `weak`. The reason behind this lies in how `weak` variables reference the object via a side table, which introduces an additional pointer hop to access the object.

In contrast, `unowned` references establish a direct link to the object, eliminating this overhead.

## Swift Object Life Cycle

> The object life cycle is well described in the source code [here](https://github.com/apple/swift/blob/3e4dd4304a0f1d892e7cf68f3b473018d927ac20/stdlib/public/SwiftShims/swift/shims/RefCount.h#L111)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d8dafxe87r8jn0zbsqcu.png)

Swift object have their own life cycle. There are five states:

- Live
- Deallocating
- Deallocated
- Freed
- Dead

In `live` state object is alive. Its reference counters are initialized to 1 `strong`, 1 `weak`, 1 `unowned`. Despite on 1 weak reference the Side Table is not created here. It's needed that the state machine is working properly.

From `live` state, object moves to `deiniting` state once strong reference counter reaches zero. The `deiniting` state means that `deinit()` is in progress. At this point `weak` references will return `nil`, if there is an associated side table. `unowned` reads trigger an assertion failure. New `unowned` can be stored. From this point, object has two destinations:

- If there are no `weak` and `unowned` references, the will move to the `dead` state.
- Otherwise, the object moves to `deinited` state.

In `deinited` state the `deinit()` has been completed and the object has outstanding `unowned` references. The object can have two destinations from this point:

- If there is no `weak` references, the object immediately go to the `dead` state.
- Otherwise, the object moves to `freed` state.

In `freed` state the object is deallocated and only the side table is exist in the memory. During this phase the `weak` reference count reaches to zero and the side table destroyed. The object transitions into the final state.

In `dead` state there is nothing left from the object, expect for the pointer to it. The pointer to the `HeapObject` is freed from `Heap`, leaving no traces of the object in the memory.

## Summary

Automatic reference counting isn't a mystical process; the more we delve into its internal workings, the more we can reduce the likelihood of memory management errors in our code.

Key points to remember:

- Weak objects refer to the side table instead of referring to an object.
- ARC is implemented on compiler level. Swift compiler inserts calls of `retain` and `release` wherever appropriate. 
- Swift object are not destroyed immediately. Instead, they have 5 phases in their life cycle.

## Links

[1] [Automatic Reference Counting Documentation](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/)
[2] [Discover Side Tables](https://maximeremenko.com/swift-arc-weak-references)
[3] [Object Life Cycle. Swift Repo](https://github.com/apple/swift/blob/3e4dd4304a0f1d892e7cf68f3b473018d927ac20/stdlib/public/SwiftShims/swift/shims/RefCount.h#L111)