Sharing LibGDX Assets Across Screens Without Memory Leaks
Own LibGDX assets in a single AssetManager held by the Game class, not by individual Screens, so shared textures and skins survive screen transitions without leaking native memory.
05 Aug 2025, 02:43 UTC

Problem: Assets Reloaded and Leaked on Every Screen Change
In a LibGDX game, a Screen represents a distinct gameplay or UI mode, such as a main menu, a level, or a settings panel. When the application switches screens, it is tempting to load textures, sounds, and atlases inside Screen.show() and dispose them inside Screen.dispose(). That pattern works for screen-only assets, but it breaks down for shared resources such as a UI skin or a game font. Every transition reloads the same native OpenGL texture data, causing frame stutter, and any asset forgotten in dispose() leaks native memory that the JVM garbage collector cannot reclaim.
The useful takeaway is to own all long-lived assets in a single AssetManager instance held by the Game class, and let each Screen borrow references to finished assets. The manager is disposed once when the application exits, not when a screen is hidden.
Requirements
- Shared assets must survive screen transitions without being reloaded.
- Screen-local assets must be released when their screen is disposed.
- Native memory for textures and sounds must be freed explicitly; relying on garbage collection is insufficient.
- Rendering must not attempt to use an asset before loading completes.
Smallest Suitable Design
Create one AssetManager in the Game class. Pass the Game reference to each Screen constructor. In Screen.show(), read finished assets from the manager. In Screen.dispose(), dispose only objects created by that screen, such as a Stage or a screen-owned SpriteBatch. Never call dispose() on an asset obtained from the shared manager. In Game.dispose(), call assets.dispose() and then super.dispose().
Trust and Data Boundaries
Rendering Thread versus Loading Thread
AssetManager loads data on a background thread by default. The boundary is the moment an asset becomes available: code must check assets.update() or call assets.finishLoading() before calling assets.get(). Reading an asset too early returns null or incomplete data, which appears as missing textures or rendering exceptions.
Java Heap versus Native Memory
A LibGDX Texture is a small Java object that wraps a native OpenGL texture. The Java garbage collector can collect the wrapper, but it does not automatically free the native texture memory. The boundary is the explicit dispose() call: without it, GPU memory grows even when the Java heap looks stable.
Screen Ownership Boundary
A Screen does not own shared assets; it borrows them. The Game class owns the AssetManager and decides when assets are unloaded. This prevents one screen from disposing a font or skin that another screen still needs.
Operational Checks
- Call assets.isLoaded("path/to/asset") before accessing an asset, or verify assets.update() returns true after an asynchronous load.
- Log assets.getProgress() during startup to confirm loading is advancing.
- Switch repeatedly between screens and observe native memory in Android Studio Profiler or a desktop JVM profiler; memory should plateau, not climb.
- Confirm Game.dispose() runs exactly once at application exit and disposes the AssetManager.
Concrete Example: Shared Skin Across Two Screens
Assumes LibGDX 1.x and a standard Game/ApplicationAdapter setup. Run the desktop launcher with ./gradlew desktop:run from the project root. No special OS permissions are required beyond normal application execution.
public class MyGame extends Game {
public final AssetManager assets = new AssetManager();
@Override
public void create() {
assets.load("ui/uiskin.atlas", TextureAtlas.class);
assets.load("ui/uiskin.json", Skin.class);
assets.finishLoading();
setScreen(new MenuScreen(this));
}
@Override
public void dispose() {
assets.dispose();
super.dispose();
}
}
public class MenuScreen implements Screen {
private final MyGame game;
private Stage stage;
private Skin skin;
public MenuScreen(MyGame game) {
this.game = game;
}
@Override
public void show() {
skin = game.assets.get("ui/uiskin.json", Skin.class);
stage = new Stage();
// populate stage using skin
}
@Override
public void render(float delta) {
stage.act(delta);
stage.draw();
}
@Override
public void dispose() {
stage.dispose(); // screen-local resource
// Do NOT dispose skin; it belongs to game.assets
}
// remaining Screen methods omitted for brevity
}
Placeholders: replace "ui/uiskin.json" and "ui/uiskin.atlas" with actual asset paths, and replace MenuScreen with your screen class. Expected check: after setScreen(), game.assets.isLoaded("ui/uiskin.json", Skin.class) returns true and the UI renders without missing textures. Risk: calling finishLoading() on the render thread blocks until loading completes, which can stall the frame; for large asset sets, load asynchronously and show a loading screen while assets.update() returns false.
Failure Modes
- A Screen disposes a shared Skin or TextureAtlas. Later screens crash or render white rectangles because the underlying native resource was freed while still referenced.
- Assets are loaded in Screen.show() without waiting for completion. The first render frame attempts to bind a null texture, producing missing graphics or an exception.
- The Game class never disposes AssetManager. Native memory leaks accumulate across play sessions and can cause out-of-memory errors on mobile.
- All assets for every level are loaded at application start. Startup time increases and RAM holds assets for levels the player may never reach.
Conditions That Would Change the Design
- If level-specific asset sets are large and mutually exclusive, replace the single manager with per-level AssetManager instances disposed when the level unloads.
- If you need hot-reloading during development, keep a separate reloadable loader outside the production AssetManager so live edits do not require a full restart.
- If targeting WebGL with tight texture memory, explicitly evict or unload unused assets instead of retaining them for the entire session.
Limitation
AssetManager does not reference-count shared assets across screens. If two screens both treat the same asset as their own and both dispose it, the result is a double-free or premature free. Ownership must be explicit: only Game disposes shared assets; screens dispose only what they created.
Practical Way to Check the Result
Add a log statement in Game.dispose() and in each Screen.dispose(). Play through the menu, gameplay, and settings screens several times. The shared assets should be disposed exactly once at exit, and native memory should remain flat across screen transitions rather than increasing with each switch.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.