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:
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:

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
}
}
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:
- Are BufferedImages known to be memory hogs?
- Is there something inside
BufferedImagethat is allocating all of this extra memory? - Why does the max amount of memory allocated increase over time (why do the spikes get bigger)?
- Is there a workaround or better practice I could implement here (still using BufferedImages)?
- If not, what is the solution? Is there a more performance-tuned method to write a render loop?
- 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!


