Skip to content

Memory Management With Swift

Published: at 02:42 PM

Introduction

This article is originally from my medium blog post.

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:

Table of contents

Open Table of contents

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:

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.

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:

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

The next picture represents an object into memory:

Representing an object into memory

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.

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.

Side Table

Side Table is created only one the following things happen:

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.

Side Table

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.

An Object Life Cycle

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

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:

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

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:

Thanks for reading

If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content.


avatar

Nikita Vasilev

A software engineer with over 7 years of experience in the industry. Writes this blog and builds open source frameworks.


Previous Post
Responder Chain, Gesture Recognizers, Hit Testing, Main Event Loop
Next Post
Algorithms Complexity: All You Should Know About It