Memory Management in Unity
On my previous article, I have tried to provide a brief layout for using the Unity Profiler. Before moving further on, I have noticed that I needed to have a better understanding regarding memory allocations. We want the GC Allocation value low, of course we want minimal garbage in our project, but I really wanted to know how we are creating these garbages and the way these garbages effect our games.
Before getting into detail Unity’s memory management system, we need to understand the memory management concept in brief. Please do not hesitate to contact me if you believe any information is misleading.
To begin with, programmable areas are divided into 3 parts in memory, namely: Stack, Heap and Static. All of these types are stored within the RAM.
Stack memory is mainly used for storing:
- the value type variables (bool, byte, char, decimal, double, float, int, long, sbyte, short, uint, ulong, ushort), structs and enums
- the references (the addresses) of objects within the Heap memory.
Stack memory is also used for assigning parameter values of methods and storing the return values of the methods. Stack memory is allocated at the initialization stage of an application and the memory size is not changeable during runtime. The memory allocation during runtime is strictly managed by the OS. This management system by the OS is highly efficient, which prevents us from gathering certain errors such as memory fragmentation.
As it is being allocated at the initialization stage; if a variable within the Stack memory results in a value out of its range (such as a byte variable with a value higher than 255 or lower than 0), it will cause a “stack overflow” exception.
The data within the Stack memory is easy to access and faster than Heap memory.
In brief, reference type variables (array, class, delegate, object, string) are stored in the Heap memory. Unlike the Stack, the variables within the Heap Memory can be resized during runtime. Thus, the size of Heap memory is variable during runtime and is not strictly controlled by the OS.
So how does the heap memory is being controlled? The heap memory is controlled by the scripting language’s runtime (Common Language Runtime for .NET Framework, Mono or IL2CPP for Unity) — a more detailed information is provided at the last part of the article). As you can guess, heap memory managers are not as efficient and perfect as the OS.
All of the objects created during runtime are being stored within the heap memory. Still, stating that the reference types stored only in the Heap memory and value types in Stack memory would be misleading.
Let’s get into some more detail. The new objects instantiated during runtime from a class (which is a reference type) by using the “new” keyword gets their place of allocation within the Heap Memory. However, as can be seen on the image above, the address(reference) of that object is still stored on the Stack memory. Each reference type variable in the Heap Memory has to have a pointer in the Stack during their lifetime. Additionally, the variables of the created object are also stored within the Stack memory.
The pointer within the Stack simply tells the computer that the application has not yet finished using it. Once an object is no longer used by the application, it’s pointer at the Stack is deleted by the OS (OS will empty up the Stack memory allocation of a method once it is finished). Since OS has no control over managing the Heap memory, this results in having unreferenced objects within the Heap; which are actually now “garbages” for the application — as they are using some memory but are not being used.
Garbage Collectors are responsible for managing the allocations within heap memory. Garbage Collectors working principle is simply determining the objects without a reference (denoting that they are no longer being used) and cleaning them up from the Heap memory — to create memory space for new allocations.
Unity’s Memory Management System
Beforehand, we had to tell the computer that we were done with an object, to manually free up the memory space used by the regarding object. If we forget to free up the memory space after we are done with an object, the resulting redundant memory usage is called “memory leakage”. As Unity defines, memory leakage is “the situation where memory is allocated but never subsequently released”.
Today, runtime systems are eased our lives by managing the heap memory; decreasing human error and increasing efficiency due to requiring lesser amount of coding. Other than memory leakage; memory corruption (end result if you try to access to a deleted object) and memory errors (trying to access to an object which has not yet allocated in the memory) are also taken to minimum thanks to runtime systems.
NET’s Garbage Collector (GC) within the Common Language Runtime (CLR) is mainly responsible for allocating and releasing the managed memory of the applications in the .NET Environment. In Unity, Unity’s Mono or IL2CPP and their GC is responsible for locating and free up the unused memory space.
As Unity defines, “the process of locating and freeing up unused memory is known as garbage collection (or GC for short)”. Thanks to GC, we do not require to manually tell the processor that we are done with an object; resulting less workload for the programmer and more efficient memory management because of the facts I have provided above. However, the work system of the Garbage Collector is not as perfect as the OS managed Stack memory.
As long as new objects are created, the memory manager demands new (unused) memory space from the heap memory. As we know, memory is not an infinite resource. The memory managers allocates the unused space as long as there’s free space in the managed memory (the managed memory is not always same size with the maximum available memory — it is determined by the memory manager according to the application needs during runtime and is expandable).
Since GC is responsible for determining the unused objects, it scans all of the objects on every frame, deleting the memory space of the unused game objects. However, once a space has been freed-up by the GC, it is not directly added to the unused memory pool. Unlike OS managed Stack memory, Unity’s GC algorithm does not relocate the existing objects, resulting that there will be gaps (resulted by the deletion) in between active objects within the memory. Thus, these gaps can only be used by the objects which has an equal or smaller size. And this is where “memory fragmentation” kicks in.
Despite the fact that total unused memory in the managed memory is enough for a new object, it may not be possible to allocate it into an existing gap. The problem that occurs if a memory block could not be fit in any existing space eventhough the total memory available is larger than the block is called “memory fragmentation”.
In the figure above illustrates a case in which GC emptied up a space by removing an unused object from the managed memory and a new object, which has a larger size than the removed object is looking for an empty space for allocation. However, the existing gap is too small — imagine trying to park a bus into a space for a car: even though there might be many gaps in between the cars enough to fit a car, we still can not park the bus in any of the free spaces.
Once this happens, Unity memory manager runs the GC in order to create and find an adequate space in the memory. If it fails to do so, the memory manager expands the heap memory. Certainly, the amount that it can expand is limited to the device’s physical memory that is running the application.
For more detail, I suggest you to take a look at Unity Manual from the links below:
Understanding the managed heap
Another common problem faced by many Unity developers is the unexpected expansion of the managed heap. In Unity, the…
Understanding Automatic Memory Management
When an object, string or array is created, the memory required to store it is allocated from a central pool called the…
Back to Unity Profiler
With the information above, I believe it is more clear to interpet Unity Profiler’s GC Allocation column. The data values provided in the GC Allocation column denotes the size of the collected garbage occured within 1 frame. In a scenario in which an object has a 1 KB of GC Alloc, this means that it is creating a garbage collection of 60 KB of memory in a second, and 3.6 MB in a minute.
Here’s Unity’s explanation on Garbage Collection:
Whenever Unity needs to perform garbage collection, it stops running your program code and only resumes normal execution when the garbage collector has finished all its work. This interruption can cause delays in the execution of your game that last anywhere from less than one millisecond to hundreds of milliseconds, depending on how much memory the garbage collector needs to process and on the platform the game is running on. For real-time applications like games, this can become quite a big issue, because you can’t sustain the consistent frame rate that smooth animation require when the garbage collector suspends a game’s execution. These interruptions are also known as GC spikes, because they show as spikes in the Profiler frame time graph.
I will be providing more tips on the Profiler on my next article.
Last but not least, the Garbage Collector has no access to the Stack memory — which means that even the fact that it will increase the processing time, Unity Profiler GC Allocation tab will not be effected by the redundant usage of Stack memory. Thus, having redundant variables/methods within the Stack Memory is still going to slow down our project. This is actually why we are deleting the unused Start/Update methods if they are empty. Even they are empty, the Stack memory will process them. Long story short, it would be a mistake to ignore the data stored within the Stack as we do not observe them in the GC Allocation tab. They may not be anything at all one by one, but still, better to eliminate them.
Given the information above, I believe it is clear enough for understanding the need for optimizing the scripts to prevent unnecessary loads on the memory; especially for applications designed for mobile devices.