Memory-efficient strategy to create a rendering loop for a Java game application?

Viewed 21

As a brief introduction, I have been a hobbyist game "developer" for several years now, but I started when I was very young and inexperienced with Java, mostly replicating code from other sources to build the foundation of my projects. Now that I have reached a vastly greater level of proficiency with Java, I more fully understand what makes a game function and the exact ins and out of the code I have been using to build my games thus far.

The basic structure of my game engines is this:

public static void main(String args[]) {
     // do basic setup stuff
     ...
     Thread tick = ... // create a thread that will enter game logic loop
     Thread render = ... // create a thread that will enter game render loop
     tick.start(); // enter game logic loop
     render.start(); // enter game render loop
}

The tick method is not of concern in this post, and as far as I am aware at the moment, it functions exactly how I need it to. My render loop is the subject of concern here. The method I have always used in my game engines to render graphics to a window (specifically, a JFrame) is roughly as follows:

public static void render() {
  BufferedImage canvas = new BufferedImage(width, height, BufferedImage.INT_RGB); // 1. create a BufferedImage that will act as a single frame to be drawn on
  while (true) { // not best practice, just an example of a simple loop to render frames at unlimited FPS
     Graphics g = canvas.getGraphics(); // 2. get a Graphics context for our BufferedImage frame

     game.render(g); // 3. this dispatches a long chain of rendering that will render each game object

     g = frame.getGraphics(); // 4. now set our Graphics object to the Graphics context for our JFrame, here named 'frame'
     g.drawImage(canvas, 0, 0, null); // 5. draw the BufferedImage frame onto the JFrame, effectively rendering one frame of the game
  }
}

This method of rendering has always worked fine for me, perhaps because I have never made a game of any extreme caliber. However, I recently started a new game project which is slightly more complex and resource-demanding, which led me to find something very unfavorable at play. I noticed the game was having significant, very visible hitches every few seconds. To find the root of the issue, I began to implement some debugging features into my code including tools to monitor memory usage. This is how I noticed that the cause of these periodic hitches was Java's garbage collection, which would run periodically as the memory got full (as expected).

What was not expected was the rate at which memory was being used, and therefore needing to be reallocated by the garbage collector. After (intensive) further testing, I found that this memory issue did not occur while running my game loop with this modification:

while (true) {
     Graphics g = frame.getGraphics(); // 1. get JFrame Graphics context
     game.render(); // 2. render the game directly onto the frame, rather than a BufferedImage
}

By printing directly onto the JFrame, rather than first on a BufferedImage and then drawing that image onto the JFrame, my memory usage significantly dropped, maintaining a level that I would have initially expected considering the relative simplicity of my game. Additionally, this change drastically increased the framerate of the game, from around 60 FPS to well over 300 FPS. I assume this is due to the way in which I was drawing frames, which was essentially implementing V-Sync (i.e., printing only complete frames, rather than printing fragments of frames at a time). Yet, I would still not expect such a large discrepancy in the time it takes for the render loop to run, considering the only change was the BufferedImage. However, you can see below the difference in performance between the two versions of the render loop:

Original method, using BufferedImage frame rendering. Method two, drawing directly onto the JFrame.

Note here that the blue graph represents memory usage, the red graph represents the time it takes to complete a game logic loop, and the green graph represents the time it takes to complete a game render loop (red and green graphs do not have absolute meaning). The magenta line is the maximum y-axis value of the blue graph. 'CURMEM' refers to the amount of memory currently in used, as a percentage of the total amount of memory allocated by the JVM (in this case, ~4GB). 'MAXMEM' keeps track of the peak memory usage over the lifetime of the application.

As you can see by the sawtooth-shaped graph of memory usage, the JVM is running its garbage collector periodically as memory gets used by the application. In the first application, this garbage collection had to reallocate a very large amount of memory, resulting in a lag spike in both the logic and rendering game loops as the program hangs. In the second application, the memory usage is significantly lower and therefore the garbage collection does not have a noticeable impact on the game performance.

One additional (possibly important?) note: The problematic application starts with relatively low memory consumption, but gets increasingly worse as the application is open for several seconds. This can be better illustrated below: Increasingly greater memory consumption.

I tried one additional test while debugging, which was to continue using the BufferedImage in rendering, but completely skip the actual game rendering and only render my debugging view. In other words:

public static void render() {
  BufferedImage canvas = new BufferedImage(width, height, BufferedImage.INT_RGB); // 1. create a BufferedImage that will act as a single frame to be drawn on
  while (true) {
     Graphics g = canvas.getGraphics(); // 2. get a Graphics context for our BufferedImage frame

     //game.render(g); // 3. *SKIP RENDERING ANY GAME OBJECTS*
     renderDebugInfo(g); // 3. render debug stuff

     g = frame.getGraphics(); // 4. now set our Graphics object to the Graphics context for our JFrame, here named 'frame'
     g.drawImage(canvas, 0, 0, null); // 5. draw the BufferedImage frame onto the JFrame, effectively rendering one frame of the game
  }
}

The result was as follows: Final method, where game rendering is skipped.

In this final test, the memory usage was also significantly lowered, and the performance was good.

With all of that said, it seems that the culprit of the high memory usage and the decrease in FPS is the BufferedImage. More specifically, as deduced from the third test, it seems it is not just the BufferedImage itself, but the actions being performed on it (i.e., drawing onto the BufferedImage) that is causing the performance issues. My question(s) therefore are:

  1. Are BufferedImages known to be memory hogs?
  2. Is there something inside BufferedImage that is allocating all of this extra memory?
  3. Why does the max amount of memory allocated increase over time (why do the spikes get bigger)?
  4. Is there a workaround or better practice I could implement here (still using BufferedImages)?
  5. If not, what is the solution? Is there a more performance-tuned method to write a render loop?
  6. Is there something else entirely that I may be overlooking here?

Of course, I understand that a professionally developed game that is extremely resource-intensive and much more elaborate will likely have a highly complex method to maximize performance and quality. I am just interested in finding a way to create a very simple but effective rendering method for a relatively small game. It certainly does not need to be perfect or utterly optimized, but I would like to understand what is going on behind the scenes with the memory usage here and how I can circumnavigate those issues.

Many thanks for anyone who read this far and anyone willing to help out!

0 Answers
Related