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

我是用于Android平台的Java性能调整互动游戏。 曾经有一段时间,垃圾收集的绘图和交互有一个呃逆。 通常不到十分之一秒,但有时在非常慢的设备上可能会达到200毫秒。

我使用ddms分析器(Android SDK的一部分)来search我的内存分配来自哪里,并从我的内部绘图和逻辑循环中消除它们。

最严重的罪犯是短循环,

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

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

我也有麻烦,我想使用Canvas.drawText显示分数和进度。 这不好,

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

因为Stringschar数组和StringBuffers将被分配到所有的工作。 如果你有几个文本显示项目,并运行框架每秒60次开始累加,并会增加你的垃圾收集打嗝。 我认为这里的最佳select是保持char[]数组并将其手动解码为intdouble ,并将string连接到开始和结束。 我想听听是否有更清洁的东西。

我知道那里一定有其他人在处理这件事。 你如何处理它,以及你发现在Java或Android上交互运行的陷阱和最佳实践? 这些gc问题足以让我错过手动内存pipe理,但不是很多。

我从事Java手机游戏……避免GC'ing对象的最好方法(反过来在某个点触发GC并杀死游戏的perfs)只是为了避免在主游戏中创build它们循环放在首位。

有没有“干净”的方式来处理这个问题,我将首先举一个例子…

通常你在屏幕上有四个球(50,25),(70,32),(16,18),(98,73)。 那么,这里是你的抽象(为了这个例子简化):

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

你“popup”消失的第二个球,你的int []变成:

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

(注意我们甚至不关心“清理”第四球(98,73),我们只是跟踪我们剩下的球的数量)。

手动跟踪对象,不幸的是。 这是如何在移动设备上的大多数当前运行良好的Java游戏上完成的。

现在为string,这是我会做的:

  • 在游戏初始化时,使用drawText(…)预渲染一次数字0到9,您保存在BufferedImage[10]数组中。
  • 在游戏初始化时,预拉一次“你的分数是:”
  • 如果“你的分数是:”真的需要重绘(因为它是透明的),然后从你预先存储的BufferedImage
  • 循环来计算分数的数字,并在“您的分数是:”之后添加每个数字(通过将您的BufferedImage[10]中的相应数字(0到9)存储他们。

这给了你最好的两个世界:你得到重用的drawtext(…)字体,并在你的主循环期间创build完全零对象(因为你避开了drawtext(…)的调用本身可能很好蠢蠢欲动,好吧,不必要的废话)。

这个“零对象创build绘制分数”的另一个“好处”是对字体进行仔细的图像caching和重用并不是真正的“手动对象分配/释放” ,它实际上只是小心的caching。

这不是“干净的”,这不是“好的做法”,但这是如何在一stream的手机游戏(如Uniwar)中完成的。

而且速度很快。 快速补给 比任何涉及创build对象的速度都快。

PS:其实如果你仔细看几个手机游戏,你会注意到,字体实际上并不是系统/ Java字体,而是专为每个游戏制作的像素完美的字体(这里我只是给你一个如何caching系统的例子/ Java字体,但显然你也可以caching/重用像素完美/位图字体)。

虽然这是一个2岁的问题

避免GC滞后的唯一方法是通过静态分配所有必需的对象(包括启动时)来避免GC本身。 预先创build所有必需的对象,而不要使它们被删除。 使用对象池来重新使用现有的对象。

无论如何,即使在对代码进行了所有可能的优化之后,您也可能会暂停。 因为除了你的应用程序代码之外,其他任何东西都还在内部创buildGC对象 ,最终会变成垃圾 例如, Java基础库 。 即使使用简单的List也可以创build垃圾箱。 (所以应该避免)调用任何Java API可能会创build垃圾。 而当你使用Java时,这些分配是不可避免的。

另外,因为Java被devise为使用GC,所以如果你真的试图避免GC,那么你将会遇到缺乏function的麻烦。 (甚至应该避免List类)因为它允许 GC,所有的库可以使用GC,所以你几乎没有任何库 。 我认为避免基于GC的GC语言是一种疯狂的尝试。

最终,唯一可行的方法是降低到可以完全控制自己的记忆水平。 如C族语言(C,C ++等)。 所以去NDK。

注意

现在Google正在发送增量(并发?)GC,可以减less很多停顿。 无论如何,增量GC意味着随着时间的推移分配GC负载,所以如果分布不理想,您仍然会看到最终的停顿。 由于较less的配料和配送操作开销的副作用,GC性能本身也会降低。

我build立了自己的无垃圾版本的String.format ,至less是那种。 你可以在这里find它: http : //pastebin.com/s6ZKa3mJ (请原谅德国的评论)。

像这样使用它:

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

一切都写入一个char[]数组。 我不得不手动实现从整数转换为string(逐位数字)来摆脱所有的垃圾。

除此之外,我尽可能使用SparseArray ,因为所有Java数据结构(如HashMapArrayList等)都必须使用boxing来处理原始types。 每当你把一个intInteger ,这个Integer对象必须由GC清理。

如果您不想按照原先的方式预渲染文本,那么drawText接受任何CharSequence ,这意味着我们可以自己实现它:

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

现在绘制分数不会导致任何分配(除非buf内部数组可能必须增长,并且无论drawText是否合适,否则在开始时可能一次或两次除外)。

在避免GC暂停至关重要的情况下,可以使用的一个技巧是在知道停顿无关紧要的地方刻意触发GC。 例如,如果在游戏结束时使用垃圾密集型“showScores”function,则用户不会被显示分数屏幕和开始下一个游戏之间额外的200毫秒延迟所分心,所以您可以拨打一旦分数屏幕被绘制, System.gc()

但是,如果你使用这个技巧,你只需要注意在GC暂停的地方不要烦人。 如果您担心手机电量耗尽,请不要这样做。

不要在多用户或非交互式应用程序中执行此操作,因为这样做很可能会使应用程序运行速度变慢。

关于迭代器分配,避免ArrayList中的迭代器很容易。 代替

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

你可以做

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