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

时间:2010-03-20 17:51:40

标签: java android garbage-collection performance

我正在使用Java平台为Android平台调整交互式游戏。偶尔会有绘图和交互进行垃圾收集的打嗝。通常它不到十分之一秒,但有时在非常慢的设备上可能会达到200毫秒。

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

最糟糕的罪犯已经完成了短暂的循环,

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

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

我也无法使用Canvas.drawText显示分数和进度。这很糟糕,

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

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

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

6 个答案:

答案 0 :(得分:55)

我参与过Java移动游戏...避免GC对象的最佳方法(反过来 em> kill你的游戏的性能只是为了避免在你的主游戏循环中创建它们。

没有“干净”的方式来解决这个问题,我首先举一个例子......

通常情况下,你会在(50,25),(70,32),(16,18),(98,73)屏幕上看到4个球。好吧,这是你的抽象(为了这个例子而简化):

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

你“弹出”第二个消失的球,你的int []变为:

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

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

遗憾地手动跟踪物体。这是如何在移动设备上大多数当前表现良好的Java游戏上完成的。

现在对于字符串,这就是我要做的事情:

  • 在游戏初始化时,仅使用 drawText(...) 预先绘制您保存在BufferedImage[10]数组中的数字0到9。
  • 在游戏初始化时,预先创建“你的分数为:”
  • 如果“你的分数是:”确实需要重新绘制(因为,它是透明的),然后从预先存储的BufferedImage重新绘制
  • 循环计算分数的数字并在“你的分数为:”之后,逐个手动地逐个添加每个数字(通过每次复制相应的数字(0到9)来自您预先存储它们的BufferedImage[10]

这为您提供了两全其美的效果:您可以重复使用 drawtext(...)字体,并且在主循环期间创建的对象完全为零(因为您躲过了对 drawtext(...)的调用,这本身可能非常糟糕地生成,好吧,不必要的废话。)

“零对象创建绘制分数”的另一个“好处”是对字体的仔细图像缓存和重用实际上不是“手动对象分配/解除分配” ,它真的只是小心缓存。

这不是“干净”,它不是“良好的做法”,而是它在一流的手机游戏中的表现(比如说,Uniwar)。

而且速度很快。快点快。比参与创建对象的任何更快。

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

答案 1 :(得分:15)

虽然这是一个2岁的问题......

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

无论如何,即使您对代码进行了所有可能的优化,也可能会出现最终暂停。因为除了您的应用代码之外的任何其他内容仍在内部创建GC对象,这最终将成为垃圾。例如, Java库。即使使用简单的List也可以创建垃圾。 (因此应该避免)调用任何Java API都可能会产生垃圾。当您使用Java时,这些分配是不可避免的。

此外,由于Java旨在利用GC,如果您真的试图避免使用GC,则会因缺少功能而遇到麻烦。 (应避免使用List类)因为允许 GC,所有库可能使用GC,因此虚拟/实际上没有库。我认为在基于GC的语言上避免使用GC是一种疯狂的试验。

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

注意

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

答案 2 :(得分:7)

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

像这样使用:

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

所有内容都写入char[]数组。我必须手动(逐个数字)实现从整数到字符串的转换,以摆脱所有垃圾。

除此之外,我尽可能使用SparseArray,因为所有Java数据结构(如HashMapArrayList等)都必须使用装箱才能处理原始类型。每次您将int打包到Integer时,GC都必须清除此Integer个对象。

答案 3 :(得分:4)

如果您不想按照提议预先呈现文字,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最多可能是什么,否则可能在开头一次或两次。< / p>

答案 4 :(得分:3)

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

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

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

答案 5 :(得分:2)

关于迭代器分配,避免使用ArrayList上的迭代器很容易。而不是

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

你可以做到

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