Garbage Collector in Android Games

John Meschke | 2019-02-04

Background

Game applications utilizing real-time simulations generally all function in a similar manner. The program runs an endless loop that captures user input and updates the simulation at our desired frame rate, usually 30 or 60 times per second. Rendering to the display is also implemented in the loop, but a common practice is to keep it independent from the update routine, allowing the system to draw to the screen as often as possible. Therefore, rendering may occur much more frequently than each simulation step.

What this means for us is that we're going to be processing a lot of data over a very short period of time and it gets more intense as we strive to get a higher frame rate. Anything that takes a significant amount of time to process during our game loop can lead to a choppy user experience, so we want to optimize for performance.

There are several programming language options when developing games for Android, but we'll choose Java for its ease of use with the rest of the Android library and its compatibility across multiple devices.

As a Java programmer, you don't often have to worry about memory management because the garbage collector cleans up any dead objects for us. As a game programmer using Java, it's different. The high frame rate and high data processing demands means allocating a lot more memory than the typical program, forcing the garbage collector work harder. Even with Android's efficient garbage collector, a high frequency of garbage collection could cause unwanted stutter. The developer documentation has a guide for best memory practices to avoid memory churn.

Problem

Let's imagine our game application is two-dimensional and we represent our game objects with rectangles. These rectangles give us information about the position of the object for collision detection and also let our sprite rendering system know where to draw the object. A rectangle serves as little more than a block of data, so here is one way a design may be implemented.

package com.onwardideas.math;

public class Vector2D
{
    public float x, y;
 
    public Vector2D()
    {
        this(0.0F, 0.0F);
    }

    public Vector2D(Vector2D vector)
    {
        this(vector.x, vector.y);
    }

    public Vector2D(float x, float y)
    {
        this.x = x;
        this.y = y;
    }
}
package com.onwardideas.math;

public class Rectangle2D
{
    public final Vector2D min, max;

    public Rectangle2D()
    {
        this(0.0F, 0.0F, 0.0F, 0.0F);
    }

    public Rectangle2D(Rectangle2D rectangle)
    {
        this(rectangle.min.x, rectangle.min.y, rectangle.max.x, rectangle.max.y);
    }

    public Rectangle2D(Vector2D min, Vector2D max)
    {
        this(min.x, min.y, max.x, max.y);
    }

    public Rectangle2D(float minX, float minY, float maxX, float maxY)
    {
        min = new Vector2D(minX, minY);
        max = new Vector2D(maxX, maxY);
    }
}

Our game objects must let the rest of the system know about its placement. However, to protect the object's data from being mutated by reference, we return a copy of the rectangle.

public Rectangle2D getPosition()
{
    return new Rectangle2D(position);
}

Somewhere in our code we need to render the object's sprite to the screen. Here's one way we can do this.

public void renderWorld(SpriteDrawer drawer, TextureCache cache)
{
    for (int i = 0; i < gameObjects.length; i++)
    {
        Rectangle2D position = gameObjects[i].getPosition();
        int textureId = gameObjects[i].getTextureId();
        Texture texture = cache.getTextureReference(textureId);

        drawer.drawSprite(position, texture);
    }
}

Each sprite rendered goes through the process of allocating memory to get a copy of the rectangle. Remember that in our game loop, this occurs many times per second. Gradually, memory builds up until the garbage collector has no choice but to free it.

When we have just a few objects in our world, this isn't much of an issue. We probably get the garbage collector to run briefly every minute or two. Now what if our game world has thousands of objects? What if we need to get this rectangle data for other purposes? What about objects other than rectangles we will need to allocate? If our program isn't designed with these considerations, we could be running garbage collection multiple times a second. The system will be working extra hard and may not be able to keep up with our demands.

Now What?

Perhaps we could forget about returning an object and just return the fragments of the objects as primitive data types. This leaves our code more error prone and we'll start seeing methods with ridiculously long parameter lists. In an object-oriented language like Java, we should be using objects to group our data and pass these objects around. This is one of those times I really wish Java had C-style structs. Alas, we can't use what doesn't exist.

Using objects is central to our design principles, so we're not going to get rid of them. The benefits outweigh the costs in my opinion.

Solution 1: Allow Mutability

Instead of returning a copy of the object from a method, just return a reference to the object in question. This unfortunately allows access to the containing object's implementation details, but as long as the methods are well documented and all the programmers using them are aware of the implications, it becomes a viable option.

public Rectangle2D getPosition()
{
    return position;
}

Even as the sole developer on the project, I couldn't bring myself to trust this code. It's too fragile. Again, it would have been nice to have C-style language features like retuning a const pointer to prevent modification.

Solution 2: Use Copy Parameters

A much more secure solution is to pass in an existing object to a method that will perform the copy. This eliminates the need to create an object on the spot, thus avoiding a memory allocation.

public void getPosition(Rectangle2D position)
{
    position.min.x = this.position.min.x;
    position.min.y = this.position.min.y;
    position.max.x = this.position.max.x;
    position.max.y = this.position.max.y;
}

It's not the greatest looking code, but it gets the job done. The downside is that the object being modified needs to be preallocated somewhere. The object calling the method is usually in charge of this task. From experience, the number of preallocated objects for this purpose isnít very significant.

When Allocation Makes Sense

So far, we've only been considering massive amounts of allocations within a tight loop. We should try to avoid excessive memory allocations where necessary but we shouldn't avoid it completely. The rectangles and sprites example makes a good candidate for preallocation and copy parameters. In other parts of the system, this is likely to complicate things.

Consider a simulated game world with a handful of objects that move about. These objects can be created and destroyed based on the player's actions. We don't anticipate adding or removing more than a few objects to the simulation every few seconds, so it would just be simpler to allocate a new object when needed and null its reference when we're done with it.

As a rule of thumb, if an object is being created and destroyed every frame, consider using a preallocation technique as shown above or a data structure like an object pool.

Conclusion

Avoiding excessive garbage collection will keep a game running smoothly. You do not have to obsessively remove all instances of object allocations within the game loop though. Profile your program for performance and see where parts of the system could benefit.