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:
- Code (text) segment - the compiled instructions for functions you wrote and functions the compiler generated (setters, getters, protocol witness thunks, and generic specializations—we’ll cover these later)
- Read-only data (ROD) - constants and string literals
- Static data - globals and static variables
- Stack - one per thread. Holds function frames for local variables, function arguments, and more
- Heap - dynamic allocations with lifetimes managed by reference counting
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:
| Offset | What is stored there |
|---|---|
| 0 | Pointer to class metadata: information about the object’s type |
| 8 | Reference-count field |
| 16 onward | Stored properties |
Strong, unowned, and weak references
| Reference | Keeps the object alive? | What happens after the object is destroyed? |
|---|---|---|
| Strong | Yes | The object stays alive while this reference is needed |
unowned | No | Reading it crashes if the object has been destroyed |
weak | No | Reading 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:
- Strong and unowned references need count updates, normally atomic when shared across threads.
- The first weak reference can add an allocation. Later count updates for that object need an extra pointer lookup.
unowneddoes not itself require a weak side table, so it can be cheaper. It can still refer to an object that already has a side table. The object must still be alive when you read the reference, or the read crashes.
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.
- Each thread has its own stack. Values on it (
x) are separate copies, so threads never see each other’sx. - The object exists once. Each thread holds only a pointer to it, and each pointer counts once in
rc. - A retain or release updates the count atomically. Only one core can have permission to write the count’s cache line at a time. If several cores repeatedly update it, they must pass that permission between them, which can make updates wait. This is why repeatedly retaining the same object on several threads can be costly.
- The header and the first stored properties are on the same cache line. Writing one of those properties can therefore delay a retain on another core, even though the two operations change different fields.
- ARC makes reference-count updates safe; it does not make property access safe. If two threads access
itemsat the same time and at least one writes it, without synchronization, that is a data race. Swift 6 concurrency checking helps catch unsafe sharing. You can share data safely by keeping it immutable, using a lock, or protecting it with an actor.Sendabletells Swift that a value is safe to pass between tasks or actors. The compiler’s checks depend on the code’s isolation and settings.
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:
| Word | Meaning |
|---|---|
| Function pointer | Address of the compiled code |
| Context pointer | Address 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
| Case | Where the captured data can live | Typical cost |
|---|---|---|
No captures, such as { $0 * 2 } or a top-level function | No context needed | Copy two words; no context allocation |
| Captures, non-escaping | The caller’s stack frame, because the closure cannot run after the call returns | Can avoid allocating captured data on the heap and retaining it |
Captures, escaping: stored, returned, or passed to @escaping | Captured data stored on the heap and managed with ARC, if it is needed after the call returns | Allocate 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?
- A captured value declared with
letcan be copied into the context. The closure has that value. - A captured
varis shared between the closure and the code outside it. If the variable must remain after the call returns, Swift can store it in a heap box: a block of memory both can access. Two closures capturing the same variable then see each other’s changes. - A captured class reference is strong by default and keeps the object alive. An object that stores a closure capturing that same object can form a retain cycle: each keeps the other alive.
[weak self]or[unowned self]changes the reference stored in the context.
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.