Skip to content
iOS Field Notes

Contents

Go back

Memory explained: from value types to closures

On this page

Let’s explain what happens in your application’s memory when you run your code. This will help us reason about your code’s efficiency and reliability. We’ll take a close look at value types, objects, ARC, and closures. In the next parts, we’ll cover protocols, actors, and threads.

What types of memory are available to your app?

At a high level, the process running your app has the following types of memory available:

There are others, but the list above is complete at a high level. You can optimize memory use in each category when writing code, but each requires a different approach.

To help your app install quickly, focus on the code segment and third-party libraries. But the heap usually matters most for keeping the application running smoothly.

Stack

A running thread has a stack. Each ordinary function call uses a stack frame: space for that call’s temporary data, such as function parameters and local variables. In the simple case, reserving and releasing it just moves the stack pointer. The compiler usually knows how much stack space the call needs. The function cannot use its stack frame after it returns; all local variables are gone.

Heap

The heap stores data that can remain after a function returns. For example, a heap object can still exist after the function that created it has finished. Every class, array, object, closure, and actor uses the heap to store its data in a heap box. We’ll cover how different types are laid out there later.

Swift uses Automatic Reference Counting (ARC) for class objects and many heap buffers. A strong reference keeps an object alive. Adding one is a retain; removing one is a release. When no strong references remain, the object is destroyed.

Reference counts shared between threads need atomic updates: other threads must see the whole update, never a partly completed one. This can cost more than an ordinary read or write. Moving a stack pointer is usually cheap. Finding heap space needs more work. Updating the same reference count repeatedly from several CPU cores can add more cost because the cores must take turns writing it.

class Feed {
    // stored properties
}

func main() {
    load()
}

func load() {
    let a = Feed()   // heap allocation; a lives in load's frame
    parse(a)
}

func parse(_ a: Feed) {
    let n = 42       // Int, the value is inline in parse's frame
    let b = a        // retain: rc 1 → 2
}                    // return: b is released (rc 2 → 1), the frame is gone
%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
block-beta
    columns 13
    block:S:6
        columns 1
        st["Stack: one per thread"]
        f1["main() frame"]
        f2["load() frame · a: Feed"]
        f3["parse() frame · n: Int, the value"]
        f4["parse() frame · b: Feed"]
        f5["free · the stack pointer is here"]
    end
    space
    block:HP:6
        columns 1
        ht["Heap: one per process, all threads"]
        fl["free list: 32 B → 64 B → 48 B"]
        ot["Feed object"]
        md["md"]
        rc["rc: strong = 2"]
        sp["stored properties"]
    end
    f2 --> md
    f4 --> md
    classDef title fill:none,stroke:none,font-weight:bold
    class st,ht,ot title
    style S fill:var(--diagram-stack),stroke:none
    style HP fill:var(--diagram-heap),stroke:none

Values and references

Value types: struct, enum, and tuple

Copying a value gives you an independent value. Changing the copy does not change the original. For a simple struct, this means copying its stored bytes:

struct Point {
    var x: Double
    var y: Double
}

func f() {
    let p = Point(x: 1, y: 2)
    var q = p
    q.x = 9                     // p.x is still 1
}

There is no separate allocation for p: Point. For a specific struct type, such as Point, Swift knows how much space its properties need and where to put them. This lets Swift store the struct inside another value. Copying a struct made only of plain values, such as Int and Double, needs no reference counting.

A struct has type metadata: information about its type, such as its size and how to copy it. This information is stored separately from the struct values.

For a simple type such as Point, the compiler can put the metadata in the executable or a library. It becomes available in memory when the program loads. Swift can also create metadata while the program runs, for example for some generic types.

%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
block-beta
    columns 7
    block:S:3
        columns 1
        st["stack frame of f()"]
        px["p.x: Double · 8 B"]
        py["p.y: Double · 8 B"]
        qx["q.x: Double · own copy"]
        qy["q.y: Double · own copy"]
    end
    space
    block:HP:3
        columns 1
        ht["heap"]
        no["nothing"]
        space
        space
        space
    end
    classDef title fill:none,stroke:none,font-weight:bold
    class st,ht title
    style S fill:var(--diagram-stack),stroke:none
    style HP fill:var(--diagram-heap),stroke:none

Reference types: class and actor

A class variable holds a pointer to an object. On a 64-bit device, that pointer is 8 bytes. The object normally lives on the heap.

struct Item {}

final class Feed {
    var token: NSObjectProtocol?
    var items: [Item] = []
}

let a = Feed()                  // create an object
let b = a                       // copy its address; keep the same object alive
b.items.append(Item())
print(a.items.count)            // 1

a and b refer to the same object. Assignment copies its address and, when needed, retains the object. It does not copy the stored properties.

The memory layout of a native Swift class object on the heap begins with a pointer to its type information and a reference-count field. Its stored properties come after those two fields. The type information helps Swift choose which method implementation to call while the program runs (dynamic dispatch). It also supports type(of:) checks.

The following drawing uses a Feed with token and items properties. The array header is inside the object; its elements live in a separate buffer.

%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
block-beta
    columns 10
    block:S:2
        columns 1
        st["stack"]
        a["a: Feed"]
        b["b: Feed"]
        space
        space
    end
    space
    block:HP:4
        columns 1
        ht["Feed object, heap"]
        md["word 0 · md → Feed class metadata"]
        rc["word 1 · rc: strong = 2"]
        tk["token: NSObjectProtocol?"]
        it["items: [Item] · header"]
    end
    space
    block:B:2
        columns 1
        space
        space
        space
        space
        buf["array buffer"]
    end
    a --> md
    b --> md
    it --> buf
    classDef title fill:none,stroke:none,font-weight:bold
    class st,ht title
    style S fill:var(--diagram-stack),stroke:none
    style HP fill:var(--diagram-heap),stroke:none
    style B fill:none,stroke:none

Automatic Reference Counting (ARC)

Swift uses ARC to manage class and actor instances, heap-allocated closure contexts, and task objects. A strong reference keeps the object alive. Swift adds retains when it needs to keep an object alive and releases when a reference is no longer needed. The compiler can remove unnecessary retains and releases, so an assignment in your code does not always cause a retain.

The object header

Typical class instance on a 64-bit device is stored as follows:

OffsetWhat is stored there
0Pointer to class metadata: information about the object’s type
8Reference-count field
16 onwardStored properties

Strong, unowned, and weak references

ReferenceKeeps the object alive?What happens after the object is destroyed?
StrongYesThe object stays alive while this reference is needed
unownedNoReading it crashes if the object has been destroyed
weakNoReading it returns nil

When the strong count reaches zero, Swift runs deinit and destroys the stored properties. Safe unowned references can keep the object’s memory reserved after that. Swift frees that memory when the unowned count allows it. The reserved memory does not make the destroyed object usable.

Why weak references need a side table

A side table is a separate small heap block with reference-count information. Creating the first weak reference normally allocates this table. Swift moves the strong and unowned counts into the table and adds a weak count. The object’s original count field then stores a pointer to that table and a flag indicating that the table is in use.

Weak references point to the table. After the object is gone, the table can still record that it is dead. Reading a weak reference then returns nil. The table remains while weak references still need it.

Once an object uses a side table, its retains and releases follow an extra pointer to reach the counts. Swift can also create a side table if a count becomes too large to fit in the object’s count field. This rarely happens in ordinary code.

The costs to remember:

Why sharing an object across threads can be expensive

A CPU cache keeps recently used memory close to a CPU core. It moves data in blocks called cache lines. If several cores update the same reference count, they must take turns getting permission to write its cache line.

%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
block-beta
    columns 3
    block:T1
        columns 1
        t1["Thread 1 · core 0 · stack"]
        x1["x: Int = 1"]
        f1["f: Feed"]
    end
    block:T2
        columns 1
        t2["Thread 2 · core 1 · stack"]
        x2["x: Int = 7"]
        f2["f: Feed"]
    end
    block:T3
        columns 1
        t3["Thread 3 · core 2 · stack"]
        x3["x: Int = 3"]
        f3["f: Feed"]
    end
    space:3
    block:HP:3
        columns 1
        ht["heap · one object · cache line A, 128 B on Apple silicon"]
        w0["word 0 · md → Feed class metadata · read only"]
        w1["word 1 · rc: strong = 3 · atomic"]
        w2["word 2 and on · stored properties · NOT atomic"]
    end
    f1 --> w0
    f2 --> w0
    f3 --> w0
    classDef title fill:none,stroke:none,font-weight:bold
    class t1,t2,t3,ht title
    style T1 fill:var(--diagram-stack),stroke:none
    style T2 fill:var(--diagram-thread-two),stroke:none
    style T3 fill:var(--diagram-thread-three),stroke:none
    style HP fill:var(--diagram-heap),stroke:none

Each thread has its own copy of x. Each core’s nearest cache, called its L1 cache, may hold a copy of cache line A.

References inside structs and copy-on-write

A struct can contain a class reference

final class User {
    var name: String

    init(name: String) {
        self.name = name
    }
}

struct Row {
    var id: Int
    var owner: User
}

let row = Row(id: 1, owner: User(name: "Alexandra Johnson"))

A Row uses 16 bytes here: 8 for id, 8 for the owner pointer. Copying it copies both words and retains the User. The two rows have separate id values but point to the same owner. Changing that owner’s properties is visible through either row.

%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
block-beta
    columns 11
    block:S:3
        columns 1
        st["stack"]
        id["row.id: Int · 8 B"]
        ow["row.owner: User · 8 B"]
        space
    end
    space
    block:HP:4
        columns 1
        ht["User object, heap"]
        md["md → User class metadata"]
        rc["rc: strong = 1"]
        nm["name: String · header"]
    end
    space
    block:B:2
        columns 1
        space
        space
        space
        sb["string buffer"]
    end
    ow --> md
    nm --> sb
    classDef title fill:none,stroke:none,font-weight:bold
    class st,ht title
    style S fill:var(--diagram-stack),stroke:none
    style HP fill:var(--diagram-heap),stroke:none
    style B fill:none,stroke:none

The String header for the name property is inside the User object. The drawing shows User.name using a separate buffer; small strings may store their characters in the header instead.

var copy = row
copy.id = 2
copy.owner.name = "Morgan Smith"

print(row.id, copy.id)             // 1 2: separate stored values
print(row.owner.name)              // Morgan Smith: shared User object
print(row.owner === copy.owner)    // true

Copying this row copies only 16 bytes, but it can still need one retain for owner. A struct needs retains for each strong reference it copies, unless the compiler can remove them.

Collections share storage until a copy changes

Array, String, Dictionary, and Set are value types that can store their contents in reference-counted heap buffers. The value itself can be small even when its buffer is large. Copying the value can copy a reference to that buffer instead of copying every element immediately.

Copy-on-write (COW) means copies can share a buffer until one needs to change it. If the buffer is still shared, the changed value gets its own buffer. This avoids copying data before a change makes the copy necessary.

Being a value type describes what happens when you copy it, not where its data lives. Changing the elements of one array or adding an element to one set leaves its copy unchanged. That is value semantics. The two collections can still share a heap buffer until one changes.

If a collection contains class references, copying it copies those references. The objects remain shared: changing an object’s property is visible through both collections.

let xs = [1, 2, 3]
var ys = xs                       // share storage for now
ys.append(4)                      // ys needs its own storage
// xs is [1, 2, 3]; ys is [1, 2, 3, 4]
%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
block-beta
    columns 7
    block:S:3
        columns 1
        st["stack"]
        xs["xs: [Int] · buffer pointer"]
        ys["ys = xs · buffer pointer"]
        space
        space
        space
        space
        space
        space
    end
    space
    block:HP:3
        columns 1
        ht["array buffer, heap"]
        md["md → array buffer metadata"]
        rc["rc: strong = 2"]
        ct["count: 3"]
        cp["capacity: 4"]
        e0["element 0: 1"]
        e1["element 1: 2"]
        e2["element 2: 3"]
        e3["slot 3: unused"]
    end
    xs --> md
    ys --> md
    classDef title fill:none,stroke:none,font-weight:bold
    class st,ht title
    style S fill:var(--diagram-stack),stroke:none
    style HP fill:var(--diagram-heap),stroke:none

In this example, the buffer has two owners. Before changing ys, Swift creates a buffer for it, copies the three elements, and then appends 4. If only one array uses the buffer and it has enough room, Swift can change that buffer directly.

isKnownUniquelyReferenced asks whether a class object has a single strong owner. It is useful when implementing your own COW storage. If the object has one strong owner, your COW implementation may be able to change its storage directly. If the storage is shared, it needs a copy before changing it.

The same behavior applies to an array stored inside a struct:

struct Scores {
    var values: [Int]
}

let original = Scores(values: [1, 2, 3])
var updated = original             // the array can share its buffer
updated.values.append(4)           // mutation separates the array storage
print(original.values)            // [1, 2, 3]
print(updated.values)             // [1, 2, 3, 4]

This is why Scores can be cheap to pass around. Its copy may retain a buffer, but it does not necessarily copy every element. Collections store their data in different ways. For example, a small string can store its characters without a separate heap buffer.

Closures: code plus captured data

A closure contains code to run and access to variables it uses from outside the closure. Using those outside variables is called capturing them. A typical Swift closure value uses two words:

WordMeaning
Function pointerAddress of the compiled code
Context pointerAddress of captured data, or nil if there is none

The compiled code is stored in the executable or a library. The context stores the data captured by this particular closure.

Three capture cases

CaseWhere the captured data can liveTypical cost
No captures, such as { $0 * 2 } or a top-level functionNo context neededCopy two words; no context allocation
Captures, non-escapingThe caller’s stack frame, because the closure cannot run after the call returnsCan avoid allocating captured data on the heap and retaining it
Captures, escaping: stored, returned, or passed to @escapingCaptured data stored on the heap and managed with ARC, if it is needed after the call returnsAllocate space for captured data and keep it alive with ARC

A non-escaping closure cannot be stored for use after the function call returns. Its captured data may stay on the stack. An escaping closure can be used later, so any data it still needs must remain available. The compiler can sometimes avoid allocations or combine them; an escaping closure does not always allocate exactly one box.

No captures. This closure uses only its argument. It needs no data from the surrounding scope:

let double: (Int) -> Int = { value in
    value * 2
}
print(double(3))                    // 6

Non-escaping capture. Closure parameters are non-escaping by default. runNow calls its closure before returning, so the captured n does not need to survive showNumber():

func runNow(_ action: () -> Void) {
    action()
}

func showNumber() {
    let n = 3
    runNow { print(n) }             // captures the local n
}

showNumber()                       // 3

Escaping capture. A returned closure can run after the method that created it returns. This example extends the Feed class declared in section 2, which has an items property:

extension Feed {
    func makeUpdater() -> () -> Int {
        var n = 0
        return {
            n += 1                 // captured variable survives makeUpdater()
            self.items.append(Item())  // captured self keeps this Feed alive
            return n
        }
    }
}

let feed = Feed()
let update = feed.makeUpdater()
print(update())                    // 1
print(update())                    // 2
print(feed.items.count)            // 2

The returned closure keeps access to both n and self. Its captured data can use heap storage so it remains available after makeUpdater() returns. The drawing below shows these three cases; the compiler may store the captured variable in a separate shared box rather than directly inside the closure context.

%%{init: {"themeVariables": {"fontSize": "16px"}}}%%
block-beta
    columns 11
    block:L:4
        columns 1
        t1["no captures"]
        fp1["fn ptr"]
        cx1["context: nil"]
        t2["non-escaping, captures n"]
        fp2["fn ptr"]
        cx2["context"]
        t3["escaping, captures n (var) and self"]
        fp3["fn ptr"]
        cx3["context"]
        space
        space
    end
    space
    block:R:3
        columns 1
        r1["binary"]
        code["function body, code"]
        space
        r2["caller's stack frame"]
        cn["n: Int, already here"]
        space
        r3["context box, heap"]
        bmd["md"]
        brc["rc"]
        bn["n: Int"]
        bs["self: Feed"]
    end
    space
    block:F:2
        columns 1
        space
        space
        space
        space
        space
        space
        space
        space
        space
        space
        feed["Feed instance"]
    end
    fp1 --> code
    fp2 --> code
    fp3 --> code
    cx2 --> cn
    cx3 --> bmd
    bs --> feed
    classDef title fill:none,stroke:none,font-weight:bold
    class t1,t2,t3,r1,r2,r3 title
    style L fill:var(--diagram-stack),stroke:none
    style R fill:var(--diagram-heap),stroke:none
    style F fill:none,stroke:none

On disk, the closure’s machine code is in the executable or a library. When the program runs, the operating system makes that code available in a part of the program’s memory called the text segment (__TEXT on Apple platforms). This memory can be executed and is usually read-only. The operating system loads code pages into physical RAM as needed.

The text segment is separate from the stack and heap. In the drawing, “binary” means this loaded code. The function pointer stores the address of the code to run.

Functions, methods, initializers, deinitializers, property getters and setters, and compiler-generated helpers also store their instructions in this code memory. Their data is stored separately. For example, Feed.makeUpdater() has its instructions in the text segment, while feed.items is stored inside the heap object.

What exactly is captured?

func makeCounter() -> () -> Int {
    var n = 0                       // shared storage that survives this call
    return { n += 1; return n }     // the closure uses that storage
}
let a = makeCounter()
let b = a                           // same closure, same captured variable
_ = a()
print(b())                          // 2

A capture list changes the rule. { [x] in ... } captures the current value of x when the closure is created, even if x is a var. Without the capture list, a closure that refers to that variable shares it. If x is a class reference, capturing its current value copies the pointer; it does not clone the object.

final class User {
    var name: String

    init(name: String) {
        self.name = name
    }
}

var x = User(name: "Alex Johnson")
let capturedValue = { [x] in x.name }
let sharedVariable = { x.name }

x.name = "Morgan Smith"
print(capturedValue())              // Morgan Smith
print(sharedVariable())             // Morgan Smith

x = User(name: "Taylor Davis")
print(capturedValue())              // Morgan Smith: keeps the original object
print(sharedVariable())             // Taylor Davis: reads the current x

Both closures initially refer to the same object, so both see changes to its name property. Assigning another object to x shows the difference: [x] keeps the original reference, while the closure without a capture list reads the shared variable’s current reference.