开发者

How can I avoid garbage collection delays in Java games? (Best Practices) [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.

Want to improve this question? Update the question so it focuses on one problem only by editing this post.

Closed 9 years ago.

Improve this question

I'm performance tuning interactive games in Java for the Android platform. Once in a while there is a hiccup in drawing and interaction for garbage collection. Usually it's less than one tenth of a second, but sometimes it can be as large as 200ms on very slow devices.

I am using the ddms profiler (part of the Android SDK) to search out where my memory allocations come from and excise them from my inner drawing and logic loops.

The worst offender had been short loops done like,

for(GameObject gob : interactiveObjects)
    gob.onDraw(canvas);

where every single time the loop was executed there was an iterator allocated. I'm using arrays (ArrayList) for my objects now. If I ever want trees or hashes in an inner loop I know that I need to be careful or even reimplement them instead of using the Java Collections framework since I can't afford the extra garbage collection. That may come up when I'm looking at priority queues.

I also have trouble where I want to display scores and progress using Canvas.drawText. This is bad,

canvas.drawText("Your score is: " + Score.points, x, y, paint);

because Strings, char arrays and StringBuffers will be allocated all over to make it work. If you have a few text display items and run the frame 60 times a second that begins to add up and will 开发者_高级运维increase your garbage collection hiccups. I think the best choice here is to keep char[] arrays and decode your int or double manually into it and concatenate strings onto the beginning and end. I'd like to hear if there's something cleaner.

I know there must be others out there dealing with this. How do you handle it and what are the pitfalls and best practices you've discovered to run interactively on Java or Android? These gc issues are enough to make me miss manual memory management, but not very much.


I've worked on Java mobile games... The best way to avoid GC'ing objects (which in turn shall trigger the GC at one point or another and shall kill your game's perfs) is simply to avoid creating them in your main game loop in the first place.

There's no "clean" way to deal with this and I'll first give an example...

Typically you have, say, 4 balls on screen at (50,25), (70,32), (16,18), (98,73). Well, here's your abstraction (simplified for the sake of this example):

n = 4;
int[] { 50, 25, 70, 32, 16, 18, 98, 73 }

You "pop" the 2nd ball which disappears, your int[] becomes:

n = 3
int[] { 50, 25, 98, 73, 16, 18, 98, 73 }

(notice how we don't even care about "cleaning" the 4th ball (98,73), we simply keep track of the number of balls we have left).

Manual tracking of objects, sadly. This how it's done on most current well-performing Java games that are out on mobile devices.

Now for strings, here's what I'd do:

  • at game initialization, predraw using drawText(...) only once the numbers 0 to 9 that you save in a BufferedImage[10] array.
  • at game initialization, predraw once "Your score is: "
  • if the "Your score is: " really needs to be redrawn (because, say, it's transparent), then redraw it from your pre-stored BufferedImage
  • loop to compute the digits of the score and add, after the "Your score is: ", every digit manually one by one (by copying each the time the corresponding digit (0 to 9) from your BufferedImage[10] where you pre-stored them.

This gives you best of both world: you get the reuse the drawtext(...) font and you created exactly zero objects during your main loop (because you also dodged the call to drawtext(...) which itself may very well be crappily generating, well, needless crap).

Another "benefit" of this "zero object creation draw score" is that careful image caching and reuse for the fonts is not really "manual object allocation/deallocation", it's really just careful caching.

It's not "clean", it's not "good practice" but that's how it's done in top-notch mobile games (like, say, Uniwar).

And it's fast. Darn fast. Faster than anything involving the creation of object.

P.S: Actually if you carefully look at a few mobile games, you'll notice that often fonts are actually not system/Java fonts but pixel-perfect fonts made specifically for each game (here I just gave you an example of how to cache system/Java font but obviously you could also cache/reuse a pixel-perfect/bitmapped font).


Though this is a 2 years old question...

The only, and the best approach to avoid GC lag is avoiding GC itself by allocating all required objects somewhat statically (including at start-up). Pre-create all the required object, and never make them to be deleted. Use object pooling to re-use existing object.

Anyway you may have eventual pause even after you did every possible optimizations on your code. Because anything else than your app code is still creating GC objects internally which will become garbage eventually. For example, Java base library. Even using of simple List class may create garbages. (so should be avoided) Calling any of Java API may create garbages. And these allocations are not avoidable while you're using Java.

Also, because Java is designed to utilize GC, you will have trouble by lack of features if you really try to avoid GC. (even List class should be avoided) Because it allows the GC, all the libraries may use GC, so you virtually/practically have no library. I regard avoiding GC on GC based language is a kind of insane trial.

Ultimately, the only practical way is going down to lower level where you can control memory yourself completely. Such as C family languages (C, C++, etc.). So go to NDK.

Note

Now Google is shipping incremental (concurrent?) GC which can decrease pause a lot. Anyway incremental GC means just distributing GC load over time, so you still see eventual pause if distribution is not ideal. Also GC performance itself will be decreased due to side-effect of less batching and distribution operation overhead.


I built my own garbage-free version of String.format, at least kind of. You find it here: http://pastebin.com/s6ZKa3mJ (please excuse the German comments).

Use it like this:

GFStringBuilder.format("Your score is: % and your name is %").eat(score).eat(name).result

Everything is written into a char[] array. I had to implement conversion from integer to string manually (digit by digit) to get rid of all garbage.

Apart from that, I use SparseArray where possible, because all the Java data structures such as HashMap, ArrayList etc. have to use boxing in order to deal with primitive types. Every time you box an int to Integer, this Integer object has to be cleaned up by the GC.


If you don't want to pre-render the text as has been proposed, drawText accepts any CharSequence which means that we can make our own smart implementation of it:

final class PrefixedInt implements CharSequence {

    private final int prefixLen;
    private final StringBuilder buf;
    private int value; 

    public PrefixedInt(String prefix) {
        this.prefixLen = prefix.length();
        this.buf = new StringBuilder(prefix);
    }

    private boolean hasValue(){
        return buf.length() > prefixLen;
    }

    public void setValue(int value){
        if (hasValue() && this.value == value) 
            return; // no change
        this.value = value;
        buf.setLength(prefixLen);
        buf.append(value);
    }


    // TODO: Implement all CharSequence methods (including 
    // toString() for prudence) by delegating to buf 

}

// Usage:
private final PrefixedInt scoreText = new PrefixedInt("Your score is: ");
...
scoreText.setValue(Score.points);
canvas.drawText(scoreText, 0, scoreText.length(), x, y, paint);

Now drawing the score doesn't cause any allocations (except maybe once or twice in the beginning when bufs internal array may have to be grown, and whatever drawText is up to).


In a situation where it is critical to avoid GC pauses, one trick you could use is to deliberately trigger GC at a point where you know that the pause doesn't matter. For example, if the garbage intensive "showScores" function is used at the end of a game, the user won't be overly distracted by an extra 200ms delay between displaying the score screen and starting the next game ... so you could call System.gc() once the scores screen has been painted.

But if you resort to this trick, you need to be careful to do it only at points where the GC pause will not be annoying. And don't do it if you are worried about draining the handset's battery.

And don't do it in multi-user or non-interactive applications because you'll most likely make the application run slower overall by doing this.


Regarding the iterator allocation, avoiding iterators on ArrayList`s is easy. Instead of

for(GameObject gob : interactiveObjects)
    gob.onDraw(canvas);

you can just do

for (int i = 0; i < interactiveObjects.size(); i++) {
    interactiveObjects.get(i).onDraw();
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜