Mastering LibGDX AssetManager: Asynchronous Loading & Reference Counting
Learn how LibGDX’s AssetManager handles asynchronous loading and reference counting to keep your game running smoothly. A concrete example shows how to queue assets, monitor progress, and manage memory across platforms.
19 Dec 2025, 10:15 UTC

Why the AssetManager matters
When a game starts, it needs to pull textures, sounds, and shaders into memory. If you load them on the main thread, the frame you’re rendering will stall until the file is read, read into RAM, and decoded. On mobile, this can trigger an “Application Not Responding” dialog. LibGDX’s AssetManager solves this by queuing loads on a background thread and only giving you the asset once it’s ready.
Core concepts
Asynchronous loading
Call load() to enqueue an asset. The manager returns immediately; the load happens in the background. Each frame you must call update()—this processes the queue and returns true when all queued assets are finished.
Reference counting
Internally the manager keeps a counter for each asset. Every load() call increments the counter; each unload() decrements it. The asset is only freed when the counter reaches zero. This protects you from accidentally destroying an asset still in use by a sprite or sound.
GPU vs Java memory
AssetManager tracks Java-side handles. When you call dispose() on an asset, the Java object is freed, but the underlying GPU texture remains until you explicitly dispose it. The manager does not automatically free GPU memory; you must do so in your game’s cleanup logic.
Practical example
Below is a minimal, platform‑agnostic snippet that demonstrates the loading loop, progress check, and reference‑counted unload. Replace {yourAssetPath} with actual filenames.
// In your ApplicationListener or Screen
AssetManager assetManager = new AssetManager();
// 1. Queue assets
assetManager.load({yourTexturePath}, Texture.class);
assetManager.load({yourSoundPath}, Sound.class);
// 2. In the render loop
@Override
public void render () {
// Advance the async queue
if (!assetManager.update()) {
// Still loading – show progress bar
float progress = assetManager.getProgress(); // 0.0 .. 1.0
System.out.println("Loading: " + (int)(progress * 100) + "%");
return; // skip game logic until ready
}
// 3. Assets are ready – retrieve them
Texture tex = assetManager.get({yourTexturePath}, Texture.class);
Sound snd = assetManager.get({yourSoundPath}, Sound.class);
// 4. Use them in your game…
}
// When a level ends or the game exits
public void dispose () {
// Unload only once per asset, even if load() was called multiple times
assetManager.unload({yourTexturePath});
assetManager.unload({yourSoundPath});
assetManager.dispose(); // frees all remaining assets
}
Testing reference counting
Load the same texture twice to see the counter in action:
assetManager.load({sharedTexture.png}, Texture.class);
assetManager.load({sharedTexture.png}, Texture.class); // counter = 2
// After update() finishes
Texture t1 = assetManager.get({sharedTexture.png}, Texture.class);
Texture t2 = assetManager.get({sharedTexture.png}, Texture.class);
// Unload once – counter = 1, asset stays
assetManager.unload({sharedTexture.png});
// Asset still available
Texture t3 = assetManager.get({sharedTexture.png}, Texture.class);
// Final unload – counter = 0, asset freed
assetManager.unload({sharedTexture.png});
Trade‑offs & limitations
- Manual unload required – If you forget to call
unload(), assets stay in memory forever, causing leaks. - GPU memory not auto‑managed – Large textures may stay resident on the GPU even after Java disposal. You must explicitly dispose the
Texturewhen it’s no longer needed. - Overhead – The reference‑counting logic adds a small runtime cost, but it’s negligible compared to the benefit of preventing frame drops.
- Custom loaders – For proprietary formats, you’ll need to implement an
AssetLoader. The manager will still handle async loading and reference counting, but you must ensure your loader correctly frees resources.
Actionable checklist
- Enqueue all assets at startup or level load using
load(). - Call
update()each frame; usegetProgress()to drive a loading screen. - Retrieve assets with
get()only afterupdate()returnstrue. - When an asset is no longer needed, call
unload()once per logical reference. - Dispose the
AssetManagerat the very end of the application to free Java handles. - For textures that should never be reloaded, keep a reference and dispose manually when the entire game ends to free GPU memory.
By following this pattern you keep frame rates stable, avoid crashes from dangling references, and maintain clear control over memory on all target platforms.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.