Digging deep into virtual memory
In “How To Reduce Your App’s Memory Footprint”, I explained how to lower your app’s memory. But sometimes, we need to dig deeper to understand the root of our issues. In order to do that, we need to know what virtual memory is, what debug tools we have, and as a result of that, general tips to help us with that mission.
What is Virtual Memory?
Different devices have a different amount of RAM, and the technique to abstract the different RAM systems to a unified, addresses system is called virtual memory.
Virtual memory is not unique to iOS. In fact, all modern operating systems use virtual memory, it was developed more than 60 years ago!
Virtual memory addresses being “virtual” means they don’t have to be aligned with any physical address you have. In fact, the virtual memory addresses are only limited by your processor architecture — a 32-bit processor can handle 4GB of addresses and 64-bit processors can handle 18 exabytes, which is 19,327,352,832 GB!
iPhones Don’t Have 19,327,352,832 GB
This shouldn’t come as a surprise to you, but iPhones don’t have that amount of RAM. Actually, you need more than one billion new MacBook pros in order to achieve that memory size.
But from the app perspective, it doesn’t really matter. Every process in your app gets this virtual memory space and can access every address in that space.
You should take into account that every virtual memory space has the same list of addresses. The same address in one process is a different physical address in other processes, so the process cannot access memory blocks on other processes.
Every virtual memory space maps a virtual address to a physical address. And since there are no devices that have 18 exabytes, the system may encounter constraints. Unlike OSX, iOS doesn’t have a backing store, meaning, iOS is not using the disk in order to keep memory data, so the system is limit only to its physical RAM size.
Clean and Dirty Pages
The virtual memory divides its space into chunks called pages. Pages are 16KB chunks that can hold data from any kind.
Data can also be contained on several pages, and a single page is able to hold more than one kind of data.
The most important thing to know here is that a page can be marked as dirty, clean or compressed.
Clean Memory can be loaded from the disk (“page out”) and contains frameworks, executable code, and read-only files.
Dirty Memory is any memory written by the app, heap allocations, singletons, global initializers, and the stack.
Compressed memory includes pages that are un-accessed and can be compressed or uncompressed according to your app usage.
Bottom line — your memory in use is your dirty + compressed memory. You can easily ignore the clean memory since you can restore it anytime.
Tools for Debugging Your Memory
Now that we know all the important terms such as virtual memory, dirty and clean memory, and paging, we can better understand the advanced tools we have to debug our memory problems.
The VM Tracker is part of the allocation instrument.
To profile your app in Instruments, select “Product” -> “Profile” in Xcode. Alternatively, you can make a long press on the “Play” button and select “Profile”.
After Instruments opens, select “Allocations”, and run the profiler.
Now you will see two instruments — one is the heap allocations and the other one is the VM Tracker. The Virtual Memory Tracker can help you track the type of memory that you’re most interested in: the dirty memory.
Most of the time, you won’t see any data on the VM Tracker at first. That’s because VM Tracker doesn’t show your virtual memory state continuously. You have to take snapshots to analyze your memory. If you tap the VM Tracker row, a “Snapshots” button will show up at the bottom of the window. Tapping on this button will let you set the snapshots interval and even take snapshots manually.
After you have a snapshot, you can see your dirty memory state over time. You can also see what objects take most of your dirty memory. If you want to dig even deeper, use VMMap, which is great for advanced memory debugging.
Command Line Tools — VMMap
VMMap, alongside Heap and Leaks, is a great command-line tool which aims to debug your memory objects on the virtual memory environment.
In order to use it, VMMap needs a memory graph file of your app. Generating one is easy — in Xcode, stop your app run by tapping the “Debug Memory Graph” button at the bottom of the window, and then under the “File” menu, select “Export Memory Graph”. This option will generate a memory graph file you can use with VMMap.
The first step when using VMMap is to get a summary of your virtual memory map.
In the terminal write:
VMMap -summary <memgraph file>
__TEXT — It contains executable code and constants
__DATA — Well, data 🙂
__OBJC — If your app contains Objective C code, this region contains Objective C Runtime library code
Shared Memory — System Libraries that are shared with other applications, such as Cocoa and OpenGL
Mapped file — This region contains file contents that are frequently accessed and are mapped to the virtual memory in order to enable faster access
Stack — Contains the stack memory, including parameters for every function call.
Dirty vs Clean
We previously mentioned dirty and clean memory, but what does it mean? Well, the simplest way to explain this is that clean memory is the memory that if lost, you are able to restore it from code or from storage.
In the VMMap summary report, you can see that the
__LINKEDIT region takes more than 100MB of virtual memory, but 0MB of dirty memory, and that’s because
__LINKEDIT region refers to symbol tables that are saved in a file, and therefore can be restored.
As you can see from the result, the memory in use is really just the dirty memory.
Let’s Debug Memory Issues
So how do take advantage of memory command tools?
Let’s say we have a memory issue within our app:
As you can see, our app takes 644MB after startup.
After we generate a memory graph file, we gonna use VMMap, to understand what type of memory causes us this problem.
VMMap -summary data.memgraph
We notice that the
MALLOG_TINY regions take 589MB together. If we want to see further details about the
MALLOC_LARGE region, we can use the
verbose flag combined with
grep to see the list of memory blocks:
vmmap --verbose data.memgraph | grep "MALLOC_LARGE"
So we located an interesting block that takes us about 76MB. To track down that object, we need to use the
Leaks tool, with
traceTree flag and the address:
leaks -traceTree 0x0000000014210f00 data.memgraph
Now, the displayed trace tree can show us that the problem is the array of a class name “Note”, in a view controller named
LoadingViewController. Good for us!
To summarize that –
Step 1 — Export memory graph to a file.
Step 2 — Run overview information on this file using
— summary flag
Step 3 — Track a large dirty region, and examine it using the
— verbose flag
Step 4 — Find a big block of data, and find its source using the
That’s really great, but how to prevent for the dirty memory in our app from getting bigger in the first place?
Reducing Dirty Memory
In order to reduce dirty memory, we need to remember that data we create dynamically in our app and keep is dirty memory.
Also, remember that if cache trades CPU for memory, you can trade memory for more CPU work or local storage.
Tips to reduce dirty memory in your app:
- Try to use constants and not variables: Swift encourages you to use
letwhen it’s possible because of safety but using
letreduce your dirty memory as well. In fact, the
letobject doesn’t count at all when country app memory since its part of the code and therefore it’s considered to be a clean memory.
- Save big data objects to local storage and load them when you need: If you can save your data to cache files and load it when you need it, you can free your dirty memory. Think of it as a global storage for big chunks of information.
- Allocate variables in the stack: Allocating variables in the stack, meaning, allocate them as a method variables, does count in the dirty memory column, but once the methods finish their run, this memory is free.
- Use the smallest data type you can: The default
Intdata type is based on the CPU architecture. In 64 bit devices, which are almost all of the active devices today, the default
Int64. So try to use
Int16when you can.
- Use Lazy Loading: It is always best practice to allocate objects only when you need them. The lazy loading in Swift is just for that. If you implement it, there are chances those objects won’t get allocated at all.
All those tips seem to be minor, but by combining them all of them you can save a fair amount of memory, and improve your user experience.
You can quietly ignore this article and continue to develop iOS Apps, but knowing this material can help you both investigate issues related to memory problems and prevent them in the first place, as a result, give your app a great boost in user experience.