Process

Stopping Garbage Collection Spikes in Unity Mobile

Those random hitches on Android are often the garbage collector cleaning up after your code. Here is how to find the allocations, the usual culprits, and the small fixes that remove them.

Vectra Play 5 min read
A profiler timeline with sharp spikes on a monitor while a phone running a game sits on the desk

Unity garbage collection spikes on mobile happen when your code allocates memory every frame and the collector stops to clean it up. To fix them, find per-frame allocations with the Profiler's GC Alloc column, remove the common culprits like string building and LINQ in Update, and make sure incremental GC is turned on.

What a GC spike looks like

The symptom is a hitch. The game runs smoothly, then freezes for a fraction of a second, seemingly at random. On low-end Android it can be bad enough that players notice every few seconds.

The cause is usually the same. Code creates small objects on the managed heap, those objects are thrown away, and eventually the garbage collector runs to reclaim the memory. When it runs, it takes time from your frame.

Step 1: turn on incremental GC

Incremental garbage collection splits the work across several frames instead of doing it all at once. It does not remove allocations, but it softens the spikes.

Check it under Project Settings > Player > Other Settings > Use incremental GC. It is enabled by default in recent Unity versions, but older projects that were upgraded may have it off.

Step 2: find the allocations

Open Window > Analysis > Profiler, connect to a development build on the phone, and select the CPU Usage module. In the Hierarchy view, sort by the GC Alloc column.

Anything that allocates every frame during normal play is a target. A few bytes per frame sounds small, but at 60 frames per second it adds up fast. The goal for steady gameplay is as close to zero per-frame allocation as you can get.

If you cannot tell where an allocation comes from, enable Deep Profile briefly, or turn on allocation call stacks in the Profiler toolbar. Both slow the game down, so use them to locate, then turn them off.

The usual culprits and their fixes

Most per-frame allocations come from a short list. Here are the ones we see most in mobile projects, with the fix for each.

CulpritWhy it allocatesFix
String building in UpdateEvery concatenation makes a new stringUpdate text only when the value changes
LINQ in hot pathsCreates enumerators and closuresPlain loops
new WaitForSeconds in coroutinesNew object every callCache it once
RaycastAll and similarReturns a new arrayUse the NonAlloc versions
gameObject.tag == "Enemy"Reads the tag as a new stringCompareTag("Enemy")
Lambdas that capture variablesAllocate a closure each timeCache the delegate or avoid the capture

Text that updates every frame

Score and timer labels are a classic source. Update them only when the number changes, and use TextMeshPro's formatted SetText, which avoids building a new string.

int lastScore = -1;

void Update()
{
    if (score != lastScore)
    {
        scoreLabel.SetText("{0}", score);
        lastScore = score;
    }
}

Coroutine waits

Creating a wait object inside a loop allocates every iteration. Cache it.

static readonly WaitForSeconds Tick = new WaitForSeconds(0.5f);

IEnumerator Pulse()
{
    while (true)
    {
        DoPulse();
        yield return Tick;
    }
}

Physics queries

Use a reusable buffer instead of a fresh array.

readonly RaycastHit[] hits = new RaycastHit[16];

void Scan(Ray ray)
{
    int count = Physics.RaycastNonAlloc(ray, hits, 50f);
    for (int i = 0; i < count; i++) Handle(hits[i]);
}

Step 3: stop spawning and destroying

Instantiating and destroying objects during play creates garbage and other costs too. Bullets, particles, pickups, and floating text are the usual suspects.

Pool them instead. Create a set at load time and reuse them. Our guide on object pooling to fix mobile stutter walks through a simple pool.

Step 4: allocate at the right time

Some allocation is unavoidable. The trick is to move it to moments when a hitch does not matter.

Allocation is not the enemy. Allocation during gameplay is. Move it to loading screens and menus.

There is also an advanced option, GarbageCollector.GCMode, which lets you disable collection during short gameplay sections. It is powerful but risky, because memory keeps growing until you turn it back on. Use it only after the basics are done, and test memory on your weakest phone.

Check UI too

Canvas rebuilds can allocate as well as cost CPU time. If your Profiler shows allocations under Canvas or layout calls, our post on Unity UI performance on mobile has the fixes.

What we would do

We would profile a two-minute play session on the slowest supported Android phone, sort by GC Alloc, and fix the top five entries. In most projects that alone removes the visible hitches.

Then we would add pooling for anything spawned during play and confirm incremental GC is on. Only if spikes remained would we look at controlling the collector directly.

The short version

If you have chased hitches for a week and want another pair of eyes on the profile, send it over.

Related reading: Debug frame drops on Android fast, Unity mobile performance on low-end Android, Android device fragmentation guide

#Unity#Mobile#Android#Development#Quality
Your turn

Got a game idea? We build it.

You bring the concept. We design, build, test and launch it, and you own 100% of the finished game.

Share Your Game Idea  →