Under Construction
Unity••17 min•182 views••

Out Of Memory in mobile games: Don’t just look GC at Alloc

Minh Khoa

Minh Khoa

Author

image.pngThe game runs fine on a powerful device. On a weaker one, after about 15 minutes of play it is kicked straight back to the home screen. No exception, no stack trace C#.

The first instinct is usually: “The code must be generating too much garbage.”

Maybe. But if you only look GC.Allocthere, you are missing a large part of the game’s memory.

It is also not correct to say “90% of OOM comes from Native Memory.” There is no universal ratio for every project. Managed, native, and graphics resources can all contribute to pushing the game over its memory budget.

Where is the memory located?

Unity distinguishes three layers of memory management:

LayerExampleWho frees it?
Managedstring, C# objects#, data in arrays, ListGC, when there are no more references
C## UnmanagedBuffer of NativeArray, NativeListBy the allocator lifecycle; usually needs Dispose()
Engine nativeTexture, mesh, audio, render target, buffer engine/pluginBy the resource lifecycle and API corresponding

This is a classification by memory management, not three separate bars. RAM Unity: Memory overview One

can still make the managed heap grow large if it keeps data forever. List<byte[]> cannot free things that the code still holds references to. GC And

only tells you how much managed memory was allocated in the frame, not the total GC.Alloc the game is holding. Seeing RAM is not enough to conclude the game uses little memory. 0 B/frame Why is there no C# log

When the operating system terminates the process because of memory pressure, the game does not get a chance to handle it like a normal exception.#?

Android has the LMK mechanism, which selects processes based in part on priority.

there is Jetsam and separate reports, which do not contain a stack trace like a normal crash report. You can check iOS on Android and Jetsam reports on ApplicationExitInfo Android iOS. Apple, However,

no C# log **does not prove that it is OOM# . A native crash can look similar; you need to confirm with the OS log first.**The first places worth checking

1. A texture that is small on disk but large in memory

A 2048 × 2048 texture,

uncompressed: RGBA32 Just 20 such textures are already about 427

2048 × 2048 × 4 byte = 16 MiB
Có đủ mipmap         ≈ 21,3 MiB

of image data, not counting copies MiB and overhead. CPU Check the size, mipmaps, and compression format on the actual device. PNG size in the project does not tell you the texture size at runtime.

enabled even when not needed

2. Read/Write For textures, this option keeps an extra data copy that

can be read, for operations such as CPU If you only use the texture for display, that copy may not be necessary. Turn it off when you do not need it, but do not assume the total GetPixels().

always drops by exactly half. RAM 3. Long music is fully decompressed TextureImporter.isReadable

loads decompressed audio data into memory. A music file that is only a few

Decompress On Load on disk may take up significantly more when running. MB With

long BGM tracks, consider Streaming: keep the buffer smaller, at the cost of read and decode work while playing. Short sound effects that need to play frequently may fit a different approach. AudioClipLoadType

4. Leave the screen, but the resources stay behind

For example, each time the shop is opened it loads preview images but does not release the Addressables handle. Or it creates RenderTexture new ones and forgets to release the old one.

Assigning a C# variable# to null null

does not mean the underlying resource is freed. You need to handle the lifecycle correctly: release handles, destroy runtime objects you own, dispose native buffers when they are no longer used. With Addressables, release reduces the reference count; memory may not be returned immediately if the bundle is still in use.

Addressables: Memory management

Find bugs with a repeated play loop

Vào gameplay → mở shop → đóng shop → về cùng vị trí

I would measure on a real weak device:

Take snapshots before and after several loops with Memory Profiler, then compare the number and size of live objects.

Sau warm-up:        350 MB
Sau 5 lần mở shop:  470 MB
Sau 10 lần:         590 MB

Hypothetical example: RAM If you have returned to the same state but textures, buffers, or objects still keep increasing steadily, you need to find what is holding them. Cache or pool usage may rise and then stabilize, so

an increase once is not enough to conclude a leak is present too. memory peak during scene transition: the old scene hasn’t been released yet, the new scene has already been loaded, plus the decompression buffer. The game may not be leaking, but it still exceeds the budget at that moment.

For the “crashes after playing for a long time” bug, the most useful question is: after each playthrough, what is the game still keeping that should have been released?