Unity’s garbage collector is old and slow. As garbage from your scripts piles up, the garbage collector will eventually run. When it does, it runs all at once instead of spreading the work out over multiple frames. This means you get a big spike on one frame causing your game to momentarily freeze. One of the best ways to get around this is to use an “object pool” to reduce the number of objects eligible for garbage collection. After all, if there’s no garbage then there’s nothing to collect! For more on this strategy as well as a class you can use to implement it, read on!

The general idea of the “object pooling” strategy is to prevent garbage from ever being created so the garbage collector has nothing to collect and therefore no need to run. Normally, when you’ve released all your references to a class instance then it will become garbage. When enough garbage piles up, the garbage collector will run and usually cause a hitch in your game’s frame rate.

To accomplish this, we use an “object pool” for two purposes. First, when an object is no longer needed the object pool can keep holding a reference to it so that it won’t be considered garbage. Second, the object pool allows for the references it holds to be reused to prevent the need to allocate more memory. Together, this means that overall memory usage stays low while no garbage is ever created.

To implement an object pool, all we really need is a list of objects. A primitive pool would therefore look something like this:

// Make an empty pool
var pool = new Stack<Person>();
 
// Make a Person directly
var person = new Person("Fred", "Flintstone");
person.SayHello();
 
// Done with the Person, put it in the object pool
pool.Push(person);
 
// Need a new Person, grab one from the pool
person = pool.Pop();
 
// Use the Person from the pool
person.First = "Barney";
person.Last = "Rubble";
person.SayHello();

This approach has several problems. First, the pool isn’t exactly safe or easy to use. If we try to Pop a person off to use and the Stack is empty, we’ll get an exception. If we put in code to check that the pool isn’t empty, we’ve cluttered up our code.

Let’s address these issues by making the pool into a class:

public class ObjectPool
{
	private Stack<Person> objects;
 
	public ObjectPool()
	{
		// Initially nothing in the pool
		objects = new Stack<Person>();
	}
 
	public Person Get()
	{
		// If empty, make a new Person
		// Otherwise, get one from the pool
		return objects.Count == 0 ? new Person() : objects.Pop();
	}
 
	public void Release(Person person)
	{
		// Add to the pool
		objects.Push(person);
	}
}
 
// Make an empty pool
var pool = new ObjectPool();
 
// Get from the pool (creates a new Person)
var person = pool.Get();
person.First = "Fred";
person.Last = "Flintstone";
person.SayHello();
 
// Done, release back into the pool
pool.Release(person);
 
// Get from the pool (reuses the last Person)
person = pool.Get();
person.First = "Barney";
person.Last = "Rubble";
person.SayHello();

This is a lot better, but still has issues. The ObjectPool can only hold Person objects. We can fix that using generics like so:

public class ObjectPool<TObject>
	where TObject : class, new()
{
	private Stack<TObject> objects;
 
	public ObjectPool()
	{
		// Initially nothing in the pool
		objects = new Stack<TObject>();
	}
 
	public TObject Get()
	{
		// If empty, make a new object
		// Otherwise, get one from the pool
		return objects.Count == 0 ? new TObject() : objects.Pop();
	}
 
	public void Release(TObject person)
	{
		// Add to the pool
		objects.Push(person);
	}
}
 
// Make an empty pool
var pool = new ObjectPool<Person>();
 
// ... usage is the same

This is way better, but still not quite where it needs to be. When we switched from directly using the new operator to using ObjectPool.Get, we lost our ability to pass instantiation parameters to the Person class. So let’s add that ability to ObjectPool by allowing for an initialization structure to be passed in to Get. We don’t need to worry about the structure itself being garbage since struct is allocated on the stack and automatically released like other local variables without going through the garbage collector. Here’s how ObjectPool looks now:

public interface IPoolableObject<TInitArgs>
	where TInitArgs : struct
{
	void Init(TInitArgs initArgs);
}
 
public class ObjectPool<TObject, TInitArgs>
	where TObject : class, IPoolableObject<TInitArgs>, new()
	where TInitArgs : struct
{
	private Stack<TObject> objects;
 
	public ObjectPool()
	{
		// Initially nothing in the pool
		objects = new Stack<TObject>();
	}
 
	public TObject Get(TInitArgs initArgs)
	{
		// If empty, make a new object
		// Otherwise, get one from the pool
		var obj = objects.Count == 0 ? new TObject() : objects.Pop();
		obj.Init(initArgs);
		return obj;
	}
 
	public void Release(TObject person)
	{
		// Add to the pool
		objects.Push(person);
	}
}
 
public struct PersonInitArgs
{
	public string First { get; private set; }
	public string Last { get; private set; }
 
	public PersonInitArgs(string first, string last)
	{
		First = first;
		Last = last;
	}
}
 
public class Person : IPoolableObject<PersonInitArgs>
{
	private string first;
	private string last;
 
	public void Init(PersonInitArgs initArgs)
	{
		first = initArgs.First;
		last = initArgs.Last;
	}
 
	public void SayHello()
	{
		Debug.Log("Hello, my name is " + first + " " + last);
	}
}
 
// Make an empty pool
var pool = new ObjectPool<Person, PersonInitArgs>();
 
// Get from the pool (creates a new Person)
var person = pool.Get(new PersonInitArgs("Fred", "Flintstone"));
person.SayHello();
 
// Done, release back into the pool
pool.Release(person);
 
// Get from the pool (reuses the last Person)
person = pool.Get(new PersonInitArgs("Barney", "Rubble"));
person.SayHello();

This step made ObjectPool a lot safer to use by ensuring that it’s always passed the parameters it needs when being initialized. It’s much harder to accidentally forget to set one of its fields—First, Last—when getting one from the pool.

Fianlly, let’s allow for pooled objects to clean themselves up. Since we’re reusing instances, we should make sure to return to a default state when released by the user. To do this, we simply add a Release function to the IPoolableObject interface yielding the final version of the object pool code. You’re free to use it in your projects according to the MIT license.

////////////////////////////////////////////////////
// Object pooling system by Jackson Dunstan
// Article: http://JacksonDunstan.com/articles/3245
// License: MIT
////////////////////////////////////////////////////
 
using System.Collections.Generic;
 
/// <summary>
/// An object that can be put in a <see cref="ObjectPool{TObject,TInitArgs}"/> 
/// </summary>
public interface IPoolableObject<TInitArgs>
	where TInitArgs : struct
{
	/// <summary>
	/// Initialize the object
	/// </summary>
	/// <param name="initArgs">Arguments to initialize with</param>
	void Init(TInitArgs initArgs);
 
	/// <summary>
	/// Release the object
	/// </summary>
	void Release();
}
 
/// <summary>
/// A pool of objects to make reusing them easier
/// </summary>
public class ObjectPool<TObject, TInitArgs>
	where TObject : class, IPoolableObject<TInitArgs>, new()
	where TInitArgs : struct
{
	/// <summary>
	/// Unused objects eligible for reuse
	/// </summary>
	private Stack<TObject> unused;
 
	/// <summary>
	/// Make the pool with no objects
	/// </summary>
	public ObjectPool()
	{
		unused = new Stack<TObject>();
	}
 
	/// <summary>
	/// Get an object. If there are unused objects from <see cref="Release"/>, one of those will be
	/// reused. Otherwise, a new one will be instantiated. The object's
	/// <see cref="IPoolableObject.Init"/> will be called in either case.
	/// </summary>
	/// <param name="initArgs">Init arguments.</param>
	public TObject Get(TInitArgs initArgs)
	{
		var obj = unused.Count == 0 ? new TObject() : unused.Pop();
		obj.Init(initArgs);
		return obj;
	}
 
	/// <summary>
	/// Release the object so it can be reused later by calling <see cref="Get"/>. A reference is
	/// kept to prevent the garbage collector from collecting it. Its
	/// <see cref="IPoolableObject.Release"/> is also called.
	/// </summary>
	/// <param name="obj">The object to release</param>
	public void Release(TObject obj)
	{
		obj.Release();
		unused.Push(obj);
	}
}

And here’s how you use it:

using UnityEngine;
 
public struct PersonInitArgs
{
	public string First { get; private set; }
	public string Last { get; private set; }
	public int Age { get; private set; }
 
	public PersonInitArgs(string first, string last, int age)
	{
		First = first;
		Last = last;
		Age = age;
	}
}
 
public class Person : IPoolableObject<PersonInitArgs>
{
	private string first;
	private string last;
	private int age;
 
	public void Init(PersonInitArgs initArgs)
	{
		first = initArgs.First;
		last = initArgs.Last;
		age = initArgs.Age;
	}
 
	public void Release()
	{
		first = null;
		last = null;
		age = 0;
	}
 
	public void SayHello()
	{
		Debug.Log("Hello, my name is " + first + " " + last + " and I'm " + age + " years old");
	}
}
 
public class TestScript : MonoBehaviour
{
	void Awake()
	{
		// Make the pool
		var personPool = new ObjectPool<Person, PersonInitArgs>();
 
		// Get a person from the pool (allocates)
		var actor = personPool.Get(new PersonInitArgs("William", "Shatner", 84));
		actor.SayHello();
 
		// Release a person back into the pool
		personPool.Release(actor);
 
		// You shouldn't use the person anymore, but this tests that it was cleared by its Release()
		actor.SayHello();
 
		// Get a person from the pool (reuses)
		actor = personPool.Get(new PersonInitArgs("Chris", "Pine", 35));
		actor.SayHello();
 
		// Get another person from the pool (allocates)
		actor = personPool.Get(new PersonInitArgs("Zachary", "Quinto", 38));
		actor.SayHello();
	}
}

There are, of course, some downsides to this approach. First and most obviously there is additional complexity over and above simply using the new operator and letting the garbage collector kick in. Second, this approach still has some code “safety” issues. As noted in the example, you can still use objects you’ve sent to Release. That could cause exceptions to be thrown or unexpected behavior to occur. There’s also the need to provide a default constructor. Since you won’t have the init arguments at this point, you’ll have to create an object that isn’t really usable. It’s definitely a violation is the “resource acquisition is initialization” idiom and yet-another price to pay with this approach.

Overall, if you’re willing to live with these downsides then an object pool can be a very effective way to stop the garbage collector from causing framerate hiccups in your game. Please feel free to take the above code, enhance it, modify it to your tastes, and use it in your game. Please share what you’ve done with it in the comments if you do!

Do you use an object pool? How does yours compare to mine? If you don’t, how do you handle garbage collection in your games? Share your thoughts in the comments!