For this article, I want to discuss a couple topics in the iOS world I’ve always found interesting: memory management, heaps, and stacks. I’ve never really understood the difference between them or how heaps and stacks relate to storing memory in Swift.
So, I decided to do some of my own research and dig deep into memory management in Swift. I hope you’re looking forward to this topic as much as I am! Let’s dive right in.
How is Memory Managed in Swift?
Swift uses ARC (Automatic Reference Counting). Some of you might find this term familiar. But if not, no worries! We’re all here to learn something new today 🙂.
Reference counting is the process of storing reference counts of initialized objects (normally instances of a class), pointers, or blocks. These are stored in memory and cleaned up—in other words, deallocated—once the objects are no longer needed.
ARC keeps track of strong references to an object, and when the count goes down to 0 (free from all strong references), the object will automatically deallocate from memory. I specifically say strong references because only strong references increase the count. Whereas weak or unowned references do not increase the count. (We’ll go into more detail about this distinction below.)
However, if the memory count of an instance never goes down to 0, this causes something that’s called a retain cycle, which leads to a memory leak.
A memory leak, as shown above, happens when the objects within the system never get deallocated from memory. This also means that the retain count of these two objects will never come down to 0, and as such they’ll always retain each other in memory!
Retain count — Represents the number of owners for a particular object.
Let’s take a small look at this example and see how the retain count increases.
The first incremental increase of the retain count happens when we initialize a Person object. We then create two more objects that refer to person1, making the retain count go up to 3 since they’re strong references by default.
How are objects never deallocated?
With the help of ARC, Swift automatically deallocates instances that are no longer used / needed (but remember, only when the reference goes down to zero).
However, if we don’t create our own resources properly (meaning your classes have the same pointer to the same instances), this can cause instances to remain in memory even after their lifecycles have ended.
This article is great to check out if you want to take an in-depth look at how a retain cycle is created in code.
How can we identify memory leaks?
Xcode has a built in memory graph debugger. It allows you to see how many reference counts you have on an object and which objects currently exist.
How can we fix memory leaks?
Both weak and unowned references do not create a strong hold on objects.
weak — An object that can become nil at any point in time. These types must be optional. And the type must be declared as a variable (not a constant) since the value may be changed (to nil).
unowned — Assumes that the object will never become nil and is defined using non-optional types.
Dangers of memory leaks
Before moving on to memory organization, let’s take a quick look at how memory leaks are dangerous to your app 🆘❗️
- Increases memory footprint of the app: The action of creating objects is constantly repeating. This causes memory to constantly grow, which leads to memory warnings and app crashes.
- Unwanted side effects: Say we have an object that starts listening to an event upon creation and stops listening to that event when the object is deallocated. If there’s a memory leak from this object, it will never die and therefore will forever listen in on this event.
- Crashes: Multiple objects altering the database / user interface /state of app can cause crashes.
You might be thinking—we already know a lot about memory management in Swift. We do! However, Swift goes deeper and separates the ways in which it stores memory under the hood. Let’s take a look at the two following methods for memory allocation.
Heaps and Stacks
Generally speaking, Swift has 2 fundamental parts for memory allocations.
Keep in mind that Swift automatically allocates memory in either the heap or the stack.
Table of contents
1. Memory that lives in the heap
2. Example of code that gets stored in the heap
3. When the heap is used
1. Memory that lives in the stack
2. Example of code that gets stored in the stack
3. When the stack is used
C. Heaps and Stacks Data structure
1. How memory management ties in to these data structures
The heap allows you to allocate memory dynamically, unlike the stack. All objects located on the heap are reference types, such as classes. Although they are more expensive to use in memory, classes are great to use as an identity and for indirect storage. You can imagine the heap like this: when you create an instance of a class, the memory is thrown right into the heap pile.
Reference types — When instantiating a new object of a class, a reference to that object is created instead of a whole new copy. This means they can be passed around and referenced by many objects! Take a look at this example below.👇🏼👇🏼
As you can see above, favoriteDrink is also modified when we change the value of secondFavorite. The reason this happens is because, under the hood, these objects are pointing to the same location in memory. So if you modify a value, all other instances that are pointing to that same object/location will also get modified!💥
What goes on in the heap?
When you create a new object, some memory gets allocated to the heap. Just like stacks, the memory goes away once it’s not being used. However, in heaps, the memory isn’t tied to functions or programs; instead, the data in the heap is global.
The heap is less memory efficient because memory is allocated to it by assigning reference pointers. All memory within the heap are instances of reference types only.
Conclusions of the heap
- It’s more dynamic but less efficient than the stack.
- Can grow and shrink in size.
- Stores reference types such as classes.
- Goes through 3 steps: allocation, tracking reference counts, and deallocation. As such, this process is less efficient when compared to stacks.
Stacks are where all value types are stored. When a function is called, all local instances in that function will be pushed on to the current stack. And once the function has returned, all instances will have been removed from the stack.
Value types — Independent instances that hold data in their own memory allocation. This is helpful in that, when you modify a value, you won’t accidentally change the value of another instance. Such as this example below.👇🏼👇🏼
Use value types when you want to compare data of the instances with the == operator or when you want your copies to have independent states. Some examples of value types are arrays, strings, dictionaries, structs, enums, etc.
What goes on in the stack?
All of our allocation happens in what’s called a function call stack. When a function exits, all the data will pop off the stack. Data will only exist while the function is still running.
Let’s take a look at what our function call stack looks like, going through the code below along with the diagram!
Before our code runs, we allocate space for point1 and point2 instances.
Step 1 — When we instantiate point1, we’re pushing it onto the stack.
Step 2 — point1 is assigned to point2, which means a new copy of point1 is created and pushed onto the stack.
Step 3 — Simply changing the value x for point2. Notice that only point2 is altered and not point1☝🏼.
Step 4 — Once we’re done executing these tasks(functions), point1 and point2 are deallocated from memory and our stack now contains no data.
If we have a function that calls another function, the execution of all functions remains suspended until the very last function returns its values. Very interesting how so similar this is to recursion! Turns out, recursion actually uses a stack implementation as well.
Conclusions of the stack
- More efficient when allocating and deallocating data.
- Stacks store value types, such as structs and enums.
- Data stored in the stack is only there temporarily until the function exits and causes all memory on the stack to be automatically deallocated.
- Makes memory lookup and access very fast from how well it is organized.
- The most frequently reserved block is the first to be freed.
Heaps and Stacks Data Structure
The ways in which heaps and stacks are used in Swift to store memory have a lot of similarity with how we use data structures. In the next article, we’re going to see what these data structures do and how both are implemented in Swift!
That’s it for today’s article! Thank you so much for sticking with me. I hope you gained some deeper understanding for iOS development and how memory gets stored in Swift. If you have any questions, comments, or concerns, please feel free to reach out to me, and I’ll get back to you as soon as I can.🙂