Memory Management Part 1: Regions, Types and Leaks

Yahya Saddiq
Dev Genius
Published in
10 min readJun 25, 2021

--

This article is based on my talks at CocoaHeads Berlin and NSLondon.

My son “Yusuf” once told me that elephants are capable to remember water sources for decades. Photo by Photos By Beks on Unsplash.

You, as a mobile software engineer should be conscious of your apps’ memory footprint. It’s recommended to check your app memory usage before delivering a fix or new feature for your app or framework.

You should be conscious to avoid retain cycles and memory leaks which are major memory-related issues that cause unwanted side effects, bad user experience and crashes.

This article will give you some insights into what’s happening under the hood of Swift language in terms of memory management and how to debug memory issues specifically leaks and retain cycles.

In this article, you’ll learn about:

  • Memory and why its management is important
  • Differences between stack and heap memory regions
  • Class and struct lifecycle
  • Reference counting
  • Automatic reference counting vs garbage collection
  • Reference cycle
  • Memory leak

Why Memory Management is Important?

Softwares need memory access to:
1. Load software’s bytecode
2. Store both software’s data values and structures
3. Load any software’s required run-time systems

Unlike Hard disk drives, RAM is not infinite. If a program keeps on consuming memory without freeing it, ultimately it will run out of memory and crash itself or even worse crash the operating system.

Therefore, instead of letting the software developer figure this out, most programming languages provide ways to do automatic memory management.

Stack vs Heap

Any software program uses 2 memory regions known as Stack and Heap. Both stack and heap have essential roles in the execution of any program.

A general understanding of what they are and how they work will help you visualize the functional differences between a class and a struct(in other words, reference semantics and value semantics) and how you should utilize memory usage.

The Stack

Stack is organized and managed by the CPU. It’s used for static memory allocation and as the name suggests it is a last in first out stack (Think of it as a stack of pancakes 🥞).

Due to this nature, the process of storing and retrieving data from the stack is very fast as there is no lookup required, you just store and retrieve data from the topmost block on it.

Multi-threaded applications usually have a stack per thread.

Local variables(value types, primitive constants) and pointers are the typical data that the system stores on the stack.

The Heap

On the other hand, the heap doesn’t organize data. It just leaves everything scattered around (like a heap of red bricks 🧱).

The heap is a large pool of memory from which the system can request and dynamically allocate blocks of memory. The system uses the heap to store instances of reference types such as Class, Closures and now Actors with Swift 5.5.

The heap doesn’t automatically destroy its data as the stack does; a programming language requires additional work to do that. This makes creating and removing data on the heap a slower process, compared to on the stack.

When we talk about memory management we are mostly talking about managing the Heap memory.

Memory, as everybody else has a good relationship with the 🥞.

Class vs Struct

In Swift, class is a reference type. A variable of a class type doesn’t store an actual instance — it stores a reference to a location in memory that stores the instance using assign-by-reference.

While a struct is a value type that stores the actual value using assign-by-copy and provides direct access to it.

Let’s break this down using an author class and a person struct.

  1. A person instance would not reference a place in memory but the value would instead belong to person exclusively.
  2. A new father instance for the same person will be assigned-by-copy and take a new place in the stack.
  3. An instance of author class will create an object in the heap and store its address in the stack.
  4. A new constant awesomeAuthor for the same auhor instance will be assigned-by-reference by holding the same address and point to the same place in memory.

Note: value types are not necessary value semantics, that’s another topic for another article. But it’s important to keep that in mind 🤔.

Reference Counting

So far, we know that reference type objects are stored on the heap. Objects on the heap are not automatically destroyed, because the heap has a dynamic nature and is trickier to manage.

Without the utility of the call stack, there’s no automatic way for a process to know that a piece of memory will no longer be in use.

In Swift, the mechanism for deciding when to clean up not-used objects on the heap is known as reference counting.

In short, reference counting gives each object a reference count that’s incremented for each constant or variable with a reference to that object and decremented each time a reference is removed.

When a reference count reaches zero, the object is abandoned since nothing in the system holds a reference to it. When that happens, Swift will clean up the object and marks that memory as free.

Let’s demonstrate this using the same author class we used above:

Creating an instance of author creates an object in the heap with a reference count of 1 and keep its address in the stack.

Creating anotherAuthor for the same author instance increments its reference count to 2.

Creating lotsOfAuthor array of same author and anotherAuthor instances increments it to 5.

Discarding anotherAuthor decrements author’s reference count to 4.

Discarding lotsOfAuthor elements, decrement it to 1

Now! Assigning a new author object to the original author variable:

  1. Decrements the original author object to zero
  2. Mark the old abandoned object’s memory block as free
  3. Creates a new author object with reference count = 1

In the above example, you don’t have to do any work to increase or decrease the object’s reference count. That’s because Swift has a feature known as automatic reference counting or ARC.

While some older languages require you to increment and decrement reference counts in your code manually, the Swift compiler adds these calls automatically at compile time.

Note: This principle of reference counting mostly applies to classes. Because, when you pass around an instance of a class in your code, you’re passing around a memory reference, which means that multiple objects point to the same memory address.

When you’re passing around value types, the value is copied. This means that the retain count for a value type is typically always one; there is never more than one object pointing to the memory address of a value type.

ARC vs Garbage Collection

If you use a low-level language like C, you’re required to manually free memory you’re no longer using yourself.

Higher-level languages like Java and C# use something called garbage collection. In that case, the runtime of the language will search your process for references to objects, before cleaning up those that are no longer in use.

ARC unlike GC supports deterministic finalization but cannot deal with reference cycles. In my opinion, once you understand weak references and how they work to break reference cycles, ARC wins out as the superior option for performance, predictability and reducing race conditions.

Deterministic Finalization

  • In ARC, When an object’s reference count reaches zero, Swift calls deinitializer before removing an object from memory.
  • Much like init is a special method in class initialization, deinit is a special method that handles deinitialization.
  • What you do in a deinitializer is up to you, an instance is not deallocated until after its deinitializer is called.

Reference Cycle

All this talk of the heap, stack, GC, ARC is good, but now, you’ll see ARC and the reference cycle in action.

I’ll use author, book and editor classes to demonstrate a reference cycle. Both the author and the editor have their list of books. and the book is linked to the author and editor.

Create an author instance Mustafa Mahmoud (a great Egyptian doctor, philosopher and author)

A book instance Dialogue with an atheist for the author (BTW, it’s a nice book to read)

An editor instance Waley.

Assign the editor to the book.

Add the book to the editor’s list of books.

and add the book to the author’s list of books too.

This is typical publishing house data model.

Then, let’s put them all in scope so that as soon as they go out of scope the references to the book, editor and author should drop to zero and deallocate.

The problem here is that at end of the scope, the references of the book, editor and author never drop and reach zero.

🤔 WHY?! Because book and author have a reference to each other and each instance keeps the other one alive, same as for book and editor.

And we made things worse by ending the scope, the stack list items will be deallocated and there are no more references to the initial objects.

This is a classic case of a reference cycle, which leads to a software bug known as a memory leak. With a memory leak, memory isn’t freed up even though its practical lifecycle has ended.

Reference cycles are the most common cause of memory leaks.

Note: ARC handles reference counting for you automatically but it’s important to watch out for reference cycles.

Note: reference cycle is called “retain cycle” in some books and online resources. both refer to the same thing!

Memory Leak

Memory Leaks are BAD!

A memory leak is a memory that was allocated at some point but was never released and is no longer referenced by your app. Since there are no references to it, there’s no way to release it and that memory location cannot be used again.

We all create leaks at some point, from newbies to senior engineers. It doesn’t matter how experienced we are.

It is paramount to eliminate them to have clean and crash-free apps.

Memory leaks might not appear until the app has been running a long time. (A memory leak is often like a slow leak in a car tire. Leave it a few hours and there’s no sign of a problem. Leave it a week and your car is grounded).

Increases App’s Memory Footprint

Memory leaks increase memory footprint by not releasing the abandoned objects. Those objects are garbage. As the action that creates those objects are repeated, the occupied memory will grow.

Introduces Unwanted Side Effects

Imagine an object that starts listening to a notification when it is created, inside its initializer. It reacts to it, altering the database, playing a video, or posting events to an analytics engine. Since the object needs to be balanced, we make it stop listening to the notification when it is released, inside deinit.

If this object leaked, the deinitializer is not called: It will never die and never stop listening to the notification. Each time the notification is posted, the object will react to it. If the user repeats an action that creates the object in question, there will be multiple instances alive. All those instances responding to the notification and stepping into each other.

In these cases, you’ll wish for a crash!

Where Leaks Come From?

Leaks may come from a 3rd party SDK or framework. Even from classes created by Apple too like CALayer or UILabel. In those cases, there isn’t much we can do, except waiting for an update or discarding the SDK.

But it is much more likely that we introduce leaks in our code 😅.

Where to Go From Here?

In this article, you’ve learned about memory regions, data types, reference cycles in classes and memory leaks.

In the next part of the series Part 2: Reference Cycles, Closures and Debugging, you’re going to learn about breaking reference cycles in both classes and closures, Xcode memory debugger and how to locate leaks using Apple’s Malloc library.

I hope you enjoyed this article. If you have any question or comment, don’t hesitate, just drop them down here 🤗.

--

--

An accomplished programmer and dedicated educator, known for his expertise in software development. Often sharing his insights on tech and personal growth.