Memory Management

Part of software development involves resource management. One such resource in memory. Swift automatically handles this, but having a clear understanding of what it’s doing will enable better performance and code quality. There are two basic methods for managing memory during a programs execution. The simplest is using a stack, but this results in certain limits that require a more complex solution known as a memory heap.

Stack Memory Management

The stack method of memory management uses an area of memory that conceptually works like a stack, thus the name. There is a pointer, a hidden variable that stores a memory address, that keeps track of where the top of the stack is in memory. When more memory is needed, the pointer is moved up to provide more space, and when that memory is no longer needed, it’s moved back down, freeing the memory to be used again.

This happens whenever a function gets called. The function needs a certain amount of memory space for its parameters and local variables. So, just before a function is executed the stack pointer is moved up enough to provide the memory needed for the function. First, the stack pointer is moved, the functions parameters are copied into this newly acquired space, and finally the function is executed. After the function returns, the stack pointer is lowered back to where it was before to allow that memory to be reused.

However, this memory strategy has some pretty significant limits. Mainly, the amount of memory that a function will use must be known statically or at compile time. This is because the compiler must known how far to move the stack pointer before the function call, but what if we needed to deal with some data of arbitrary size? For this, we need a different approach; that approach is the heap.

Heap Memory Management

Heap memory management works in a fundamentally different way than the stack. With heap memory, we must manually ask the heap management system to allocate memory for us when we need it and then tell it when we’re done with the memory so it can be released and reused. If we never let the system know that we were finished with some memory, we would eventually run out of memory as we’d continue to allocate it but never release. This situation is known as a memory leak.

This process of having to allocate memory and then remember to release it tends to be error prone. It’s quite easy to inadvertently fail to release memory and cause a memory leak. To address this pitfall, most modern languages have some kind of automatic heap memory management system that tries to reduce the chances of a memory leak occurring by automatically deallocating heap memory that is no longer needed. There are two basic techniques to accomplish this garbage collection and automatic reference counting. Swift uses the latter, while many other languages such as Java use garbage collection.

Garbage Collection Memory Management

Systems that use garbage collection to manage memory use a graph traversal style algorithm to determine what memory can to longer be accessed from the program and then automatically release that memory. The system keeps track of all of the addresses of memory that has been allocated. Periodically, during the program’s execution, the memory management system follows the pointers on the stack and keeps track of all of the pointers it encounters as it follows every pointer it finds. Once it has followed every pointer, whatever pointers it knows are allocated, but it did not ever find in its search must no longer be accessible to the program and can be safely released.

Garbage collections major disadvantage is that this periodic search for unneeded memory requires a pause in the program’s execution, and exactly when such searches happen is indeterminate for the programmer. Thus, garbage collection memory management systems can be difficult to reason about in terms of performance. This is especially true in a program that has some real time requirements.

Automatic Reference Counting Memory Management

An alternative strategy for memory management, and the one used by Swift, is automatic reference counting (ARC). In this method, each bit of memory that is allocated tracks the number of pointers or references to it. This value, known as its retain count, is then automatically incremented and decremented as pointers to it are created and destroyed. When memory’s retain count reaches zero, there are no pointers referencing it, and it can then be safely released.

One advantage of this approach is that memory is released immediately at the point where its retain count reaches zero, and so there is no need for the memory management system to periodically pause the program to search for unneeded memory. This leads to more predictable performance and is much better for real time systems.

There is a major disadvantage of the technique though. It is possible to have to areas of memory referencing each other while none of them is referenced by the rest of the program. Their retain counts would remain nonzero and they would not be released, but since the rest of the program cannot reach them, they are no longer useful and are just leaked memory. This kind of memory leak is known as a reference cycle.

Reference Cycles

Because reference cycles can result in memory leaks, we must understand the situations in which they typically are formed and the techniques that can be used to prevent them.

The first way is when you have a parent-child relationship where the parent has a reference to the child and the child has a reference back to the parent. Since these two objects reference each other, there retain counts will never reach zero and they will never be released.

class Parent {
  var child: Child
  
  init(child: Child) {
    self.child = child
  }
}

class Child: {
  var parent: Parent?
}

This can be fixed by making the reference to the parent from the child a weak reference.

class Parent {
  var child: Child
  
  init(child: Child) {
    self.child = child
  }
}

class Child: {
  weak var parent: Parent?
}

A weak reference in Swift is a reference that does not increment the target’s retain count but still references that instance. By using a weak reference in the child, the parent’s retain count will hit zero as soon as there are no other parts of the program referencing it since the child’s reference does not count towards its retain count.

The second way that reference cycles commonly happen is when a closure is retained by an instance and that closure captures self.

class RetainCycle {
  var message: String?
  
  lazy var setMessageToNil = {
    self.message = nil
  }
}

Here the property setMessageToNil is a closure of type () -> (). (It must be declared lazy so that self is available with in the closure.) Since self is used within setMessageToNil​ the closure is said to capture self since to now has a reference to its address in memory. Since RetainCycle references setMessageToNil and setMessageToNil references self which is of type RetainCycle, we have a retain cycle and a memory leak. We can’t fix this by making setMessageToNil to a weak reference because we need to retain that closure. Instead, we must somehow make self inside the closure a weak reference. This is accomplished through the use a capture lists.

Capture Lists

When creating closures, you can declare how any values or references declared in the outer scope and used within the closure are captured by that closure. This is done with the use of a capture list.

Capture lists can be used to capture by value instead of reference. Consider the difference in behavior between the following:

var counter = 0
(1...10).forEach { _ in counter += 1 }
// counter == 10

and this which won’t even compile.

var counter = 0
(1...10).forEach { [counter] _ in counter += 1 }

By listing counter in square brackets before the parameters to the closure, we tell Swift that we wish to capture counter by value instead of by reference. This results in counter being immutable making our code no longer compile since counter += 1 attempts to mutate the now immutable counter.

It’s also possible to rename variables in our capture list.

var counter = 0
(1...10).forEach { [counting = counter] _ in print(counting) }
// counter == 0

Capture lists can also be used to specify that we wish to capture a reference without retaining it.

class RetainCycle {
  var message: String?
  
  lazy var setMessageToNil = { [weak self] in
    self?.message = nil
  }
}

By placing weak self in our capture list self inside the closure is a weak reference.

Note: self also becomes an optional since all weak references must be optional. This is because since the weak reference is not retaining the instance, it could be deallocated at anytime. If this happens are optional weak reference will simply become nil.

Although ARC significantly reduces the attention we must give to memory management, there is still the possibility of memory leaks through reference cycles so care must be used in situations that can potentially cause them. There are really only a few code patterns that can potentially cause problems and those can be avoided with proper coding practices.