Garbage Collection in Unity: Where the Hitches Come From
The stutter that ruins a smooth game is often not slow code but forgotten memory: small allocations made every frame pile up until the garbage collector must stop everything to clear them away.

Players notice certain flaws long before they can name them. The game runs smoothly, the camera glides, the frame counter holds steady, and then, for no visible reason, everything catches for a fraction of a second, as if the world had drawn a sharp breath. A moment later all is well again, until it happens once more, a minute on. Such hitches rarely come from the code that is running at that instant. More often they are the bill for thousands of tiny decisions made earlier, each one too small to see.
The bill is presented by the garbage collector, the part of the runtime that reclaims memory your scripts no longer use. C# frees the programmer from the old discipline of releasing every allocation by hand, and that freedom is real and valuable. But it is not free in time. Somewhere, at some moment of the collector's choosing, the work of finding and reclaiming dead objects has to be done, and in a game that moment arrives in the middle of a frame that was supposed to take sixteen milliseconds.
Understanding where these pauses come from means understanding three things in turn: how Unity organizes managed memory, how its collector decides what is garbage, and which ordinary looking lines of C# produce garbage without announcing it. None of this requires exotic knowledge. It requires only the patience to look at familiar code with suspicion, and the willingness to let the Profiler, rather than intuition, say where the memory goes.
The managed heap
Unity runs your scripts in a managed runtime, either Mono or, in builds that use IL2CPP, a runtime compiled ahead of time to native code. In both cases, objects you create with new, whether instances of your own classes, arrays, strings or delegates, live in a region called the managed heap. Value types such as int, float, Vector3 and your own structs normally live elsewhere, inside the variables or arrays that hold them, and creating them does not touch the heap at all. That distinction between reference types and value types is the root of nearly everything that follows.
When the heap has no free block large enough for a new allocation, the runtime has two options. It can run the garbage collector in the hope of freeing space, or it can ask the operating system for more memory and grow the heap. Unity's runtime does both, typically collecting first and expanding if that is not enough. The heap rarely shrinks back once it has grown, so a single burst of allocation during a loading screen can leave a game with a larger memory footprint for the rest of the session.
It helps to remember that engine objects such as GameObjects, Textures and Meshes are split between two worlds. The heavy data lives in native memory managed by the engine's C++ core, while your scripts hold a small managed wrapper that points to it. Destroying a GameObject releases the native side; the wrapper remains on the managed heap until nothing references it and the collector reclaims it. This is why a destroyed object can still be compared to null in a strange way, and why native and managed memory have to be studied separately.
How the collector works
Unity uses the Boehm garbage collector, a long established design that is conservative, non generational and non compacting. A collection begins by marking: the collector starts from roots such as static fields and the stacks of running threads, follows every reference it finds, and marks each object it reaches as alive. Everything left unmarked is unreachable and therefore garbage, and its memory is returned to the free lists. Conservative means the collector may treat some values that merely look like pointers as real references, so a little memory may occasionally be kept longer than necessary.
Non generational means the collector does not divide objects by age. Many modern collectors exploit the observation that most objects die young, scanning recent allocations often and old ones rarely. Boehm, as Unity uses it, examines the whole heap on each collection, so the cost of a collection grows with the amount of live data, not merely with the amount of garbage. A game holding a large, long lived world in managed memory pays for walking that world every time the collector runs, even if only a handful of temporary strings have died.
Non compacting means surviving objects are never moved together to close the gaps between them. Over time, as objects of different sizes are freed, the heap becomes fragmented, a scattering of small holes between survivors. A request for a large array may then fail to fit anywhere despite plenty of free memory in total, which forces the heap to grow. Fragmentation is a slow, unglamorous problem, and it is one more reason why allocating and discarding many objects of varying sizes during play is worth avoiding.
Where garbage comes from
Strings are the most common source, because they are immutable. Every concatenation creates a brand new string, and the old ones become garbage at once. A line in Update that builds a label from a score, such as joining the word Score with an integer and assigning it to a text component, allocates a fresh string every frame whether or not the score has changed. The remedies are simple: update the text only when the value changes, use a cached StringBuilder for repeated assembly, or use text components that accept numbers without building intermediate strings.
Boxing is subtler. When a value type is treated as an object, for example when an int is passed to a method whose parameter is of type object, or to string.Format with its params array of objects, the runtime copies the value into a new heap allocation. The code looks innocent, and no new keyword appears anywhere, yet garbage is created. Calling a method declared on an interface through a struct that has been stored in an interface typed variable has the same effect, because the struct must be boxed to live behind the interface.
LINQ and lambdas complete the usual suspects. A query such as Where followed by Select allocates enumerator objects and often delegate instances, and calling ToList on the result allocates a list and its backing array as well. A lambda that captures a local variable becomes a closure: the compiler creates a hidden class to hold the captured variable, allocates an instance of it, and allocates a delegate pointing into it. Inside a method called once at startup this is harmless. Inside a method called by a hundred enemies every frame, it becomes a steady drizzle of garbage.
Engine APIs can allocate too. Properties that return arrays, such as Mesh.vertices, hand back a new copy on every access, so reading one inside a loop multiplies the cost. Many query methods have non allocating forms: Physics.RaycastNonAlloc writes results into an array you provide, and GetComponents has an overload that fills an existing list. Reaching for these variants in hot code paths is a small habit that pays steadily.
Incremental collection
By default a Boehm collection is a stop the world event: the main thread halts until marking and sweeping are complete. For a large heap this pause can run to several milliseconds or more, which is exactly the hitch players feel. Unity therefore offers incremental garbage collection, an option in the Player Settings. With it enabled, the collector performs its marking in small slices spread over several frames, using whatever time remains in each frame after your game's work is done, rather than seizing one frame and holding it.
Incremental mode is not a cure for allocation; it is a way of distributing the cost. The total work is roughly the same, and in some cases slightly higher, because the collector must track references that change while marking is in progress, using write barriers that add a small overhead to certain operations. If a game allocates so quickly that the collector cannot keep pace in the time available, it will fall back to a full, blocking collection anyway. What incremental collection buys is smoothness for games whose allocation rate is moderate and whose frames have some slack.
There is also the option of controlling collection by hand. Calling System.GC.Collect at a moment the player will not notice, such as during a loading screen or a fade to black between scenes, clears out garbage before the action resumes. Unity additionally exposes GarbageCollector.GCMode in the Scripting namespace, which allows the collector to be disabled entirely during a critical stretch. That is a sharp tool, since with collection disabled the heap simply grows, and it suits only games that allocate almost nothing during play.
Finding the culprits
Guessing at allocations is a poor substitute for measuring them. The CPU Usage module of the Unity Profiler includes a GC Alloc column in its Hierarchy view, showing how many bytes each function allocated during the selected frame. Sorting by that column usually points straight at the offenders. Allocations also appear as GC.Alloc samples, and with call stacks enabled for them the Profiler will show exactly which line of which script requested the memory, which turns a vague suspicion into a specific fix.
Profiling a development build on the target device matters, because the editor itself allocates and adds noise, and because a phone and a desktop have very different tolerances for a collection pause. The Memory Profiler package goes further, taking snapshots of the whole heap so you can see which objects are alive and what holds them there. Between the two, the Profiler answers where garbage is born and the Memory Profiler answers why memory never goes away.
A sense of scale makes the goal clear. A function that allocates one kilobyte per frame at sixty frames per second produces sixty kilobytes of garbage per second, or 3,600 kilobytes every minute, from a single line that might look entirely harmless. Multiply that by a few dozen components, and the collector will be summoned often. In the night scenes of Crown & Ashes, when many of the Hunger are on screen at once, such per creature waste would add up quickly, which is why code that runs for every enemy every frame deserves the closest scrutiny.
Allocation as a habit
The practical discipline comes down to a single rule held firmly: steady state gameplay should allocate as close to nothing per frame as possible. Allocation at load time, when a level is built and assets are prepared, is acceptable and often unavoidable. What matters is the loop that runs while the player is playing. Caching collections in fields and clearing them instead of creating new ones, avoiding string building in Update, and replacing LINQ in hot paths with plain loops are modest changes that together eliminate most per frame garbage.
Structs can help when used with care, since an array of structs is one allocation rather than one per element, but large structs passed by value cost copying time, and structs used through interfaces box. Object pooling, which reuses instances instead of creating new ones, applies the same thinking to GameObjects and to plain C# objects alike. Unity's own collections in the UnityEngine.Pool namespace, such as ListPool, exist precisely so that temporary lists can be borrowed and returned rather than allocated and discarded.
None of this should become superstition. A few allocations in a menu, or in an event that fires once a minute, will never be felt, and rewriting readable code to avoid them is wasted effort. The aim is to know where memory is being spent, to measure before changing anything, and to concentrate attention on the small number of paths that run constantly. The hitch that seemed to come from nowhere always came from somewhere, and with the Profiler open, that somewhere usually turns out to be a few lines long.


