如何避免 Java 游戏中的垃圾收集延迟?(最佳实践)

我正在为 Android 平台使用 Java 调优交互式游戏的性能.有时,垃圾收集的绘图和交互会出现问题.通常它不到十分之一秒,但有时在非常慢的设备上可能高达 200 毫秒.

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.

我正在使用 ddms 分析器(Android SDK 的一部分)来搜索我的内存分配来自哪里,并将它们从我的内部绘图和逻辑循环中删除.

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);

每次执行循环时都会分配一个 iterator.我现在为我的对象使用数组 (ArrayList).如果我想要内部循环中的树或哈希,我知道我需要小心甚至重新实现它们,而不是使用 Java Collections 框架,因为我负担不起额外的垃圾收集.当我查看优先级队列时,可能会出现这种情况.

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.

我也遇到了麻烦,我想使用 Canvas.drawText 显示分数和进度.这很糟糕,

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);

因为 Stringschar 数组和 StringBuffers 将全部分配以使其工作.如果您有几个文本显示项目并每秒运行 60 次框架,那么就会开始累加,并会增加您的垃圾收集打嗝.我认为这里最好的选择是保留 char[] 数组并手动解码您的 intdouble 并将字符串连接到开头和结尾.我想知道是否有更清洁的东西.

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.

我知道肯定还有其他人在处理这个问题.您如何处理它,您发现在 Java 或 Android 上以交互方式运行的陷阱和最佳实践是什么?这些 gc 问题足以让我怀念手动内存管理,但不是很多.

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.

推荐答案

我从事过 Java 手机游戏...避免 GC'ing 对象的最佳方法(反过来 shall 触发GC 在某个时间点或另一个点,shall 杀死你的游戏的性能)只是为了避免首先在你的主游戏循环中创建它们.

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

通常,例如,屏幕上有 4 个球在 (50,25), (70,32), (16,18), (98,73).好吧,这是你的抽象(为了这个例子而简化了):

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 }

你弹出"消失的第二个球,你的 int[] 变成:

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

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

(请注意,我们甚至不关心清理"第 4 个球 (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).

可悲的是,手动跟踪对象.这就是目前大多数移动设备上表现良好的 Java 游戏的做法.

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:

  • 在游戏初始化时,使用 drawText(...) 仅预绘制一次您保存在 BufferedImage[10] 中的数字 0 到 9数组.
  • 在游戏初始化时,预画一次你的分数是:"
  • 如果你的分数是:" 真的需要重新绘制(因为,比如说,它是透明的),然后从你预先存储的 BufferedImage
  • 循环计算分数的位数,并在你的分数是:"之后,每个数字手动一个一个地添加(通过每次复制相应的数字(0到9)来自您预存储它们的 BufferedImage[10].
  • 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.

这为您提供了两全其美:您可以重用 drawtext(...) 字体,并且您在主循环期间创建了零个对象(因为您也 回避了对 drawtext(...) 的调用,它本身可能很可能会产生糟糕的,嗯,不必要的废话).

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.

这不是干净",也不是好习惯",但这就是顶级手机游戏(例如 Uniwar)中的做法.

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.

PS:实际上,如果您仔细查看一些手机游戏,您会发现通常字体实际上不是系统/Java 字体,而是专门为每个游戏制作的像素完美字体(这里我只是给您一个如何缓存系统/Java 字体的示例,但显然您也可以缓存/重用像素完美/位图字体).

相关文章