如何避免Java游戏中的垃圾收集延迟?(最佳做法)
我正在为Android平台调整Java交互式游戏的性能。偶尔在垃圾收集的绘图和交互过程中会出现问题。通常它不到十分之一秒,但在速度非常慢的设备上,有时它可以达到200毫秒 我正在使用ddms profiler(Android SDK的一部分)搜索内存分配的来源,并从内部绘图和逻辑循环中删除它们 最严重的违规者是短环,就像如何避免Java游戏中的垃圾收集延迟?(最佳做法),java,android,garbage-collection,performance,Java,Android,Garbage Collection,Performance,我正在为Android平台调整Java交互式游戏的性能。偶尔在垃圾收集的绘图和交互过程中会出现问题。通常它不到十分之一秒,但在速度非常慢的设备上,有时它可以达到200毫秒 我正在使用ddms profiler(Android SDK的一部分)搜索内存分配的来源,并从内部绘图和逻辑循环中删除它们 最严重的违规者是短环,就像 for(GameObject gob : interactiveObjects) gob.onDraw(canvas); 每次执行循环时,都会分配一个迭代器。我现在正
for(GameObject gob : interactiveObjects)
gob.onDraw(canvas);
每次执行循环时,都会分配一个迭代器。我现在正在为我的对象使用数组(ArrayList
)。如果我希望在内部循环中使用树或哈希,我知道我需要小心,甚至需要重新实现它们,而不是使用Java Collections框架,因为我负担不起额外的垃圾收集。当我查看优先级队列时,可能会出现这种情况
在使用Canvas.drawText
显示分数和进度时,我也遇到了问题。这很糟糕
canvas.drawText("Your score is: " + Score.points, x, y, paint);
因为Strings
,char
数组和StringBuffers
将被分配到所有位置以使其工作。如果您有一些文本显示项,并且每秒运行帧60次,则开始累积,这将增加您的垃圾收集中断。我认为最好的选择是保留char[]
数组,手动将int
或double
解码到数组中,并将字符串连接到开头和结尾。我想听听有没有更干净的
我知道肯定还有其他人在处理这件事。您是如何处理的?您发现在Java或Android上以交互方式运行的陷阱和最佳实践是什么?这些gc问题足以让我错过手动内存管理,但不是太多。我曾经在Java手机游戏上工作过。。。避免GC’ing对象(这反过来会在某个点触发GC并杀死游戏的性能)的最好方法就是首先避免在主游戏循环中创建它们
没有“干净”的方法来处理这个问题,我将首先给出一个例子
通常情况下,比如说,屏幕上有4个球(50,25),(70,32),(16,18),(98,73)。好的,这里是您的抽象(为了本例而简化):
你“弹出”第二个消失的球,你的int[]变成:
n = 3
int[] { 50, 25, 98, 73, 16, 18, 98, 73 }
(请注意,我们甚至都不关心“清洁”第四个球(98,73),我们只是跟踪剩下的球的数量)
遗憾的是,手动跟踪物体。这是如何在移动设备上运行的大多数当前性能良好的Java游戏上实现的
现在对于字符串,我要做的是:
- 在游戏初始化时,仅使用drawText(…)对保存在
BuffereImage[10]
数组中的数字0到9进行一次预绘制
- 在游戏初始化时,预先绘制一次“你的分数是:”
- 如果“您的分数是:”确实需要重新绘制(因为,比方说,它是透明的),那么请从预存储的
BufferedImage
- 循环计算分数的位数,并在“您的分数为:”之后逐个手动添加每个位数(每次复制
BufferedImage[10]
中预存储的相应位数(0到9)
这给了你最好的两个世界:你得到了重用drawtext(…)字体,你在主循环中创建了零个对象(因为你还避开了对drawtext(…)的调用,而drawtext(…)本身很可能会产生不必要的垃圾)
这种“零对象创建绘制分数”的另一个“好处”是,仔细的图像缓存和字体重用并不是真正的“手动对象分配/释放”,它只是仔细的缓存
它不是“干净的”,也不是“良好的实践”,但在顶级手机游戏(比如Uniwar)中就是这样做的
而且它很快。该死的快。比任何涉及物体创造的东西都快
附言:事实上,如果你仔细看一些手机游戏,你会发现通常字体实际上不是系统/Java字体,而是专门为每个游戏制作的像素完美字体(这里我只是给了你一个如何缓存系统/Java字体的示例,但显然你也可以缓存/重用像素完美/位图字体).如果您不想按照建议预先呈现文本,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
s内部数组时的一次或两次,以及drawText
的任何分配).在避免GC暂停非常关键的情况下,您可以使用的一个技巧是在您知道暂停无关紧要的时候故意触发GC。例如,如果垃圾密集型的“showScores”函数在游戏结束时使用,用户不会因为显示记分屏幕和开始下一场游戏之间额外的200毫秒延迟而过度分心……因此,在绘制记分屏幕后,您可以调用System.gc()
但是如果你使用这个技巧,你需要小心地只在GC暂停不会让人讨厌的地方使用。如果你担心耗尽手机的电池,就不要这样做
不要在多用户或非交互式应用程序中这样做,因为这样做很可能会降低应用程序的整体运行速度。我自己创建了一个无垃圾版本的String.format
,至少有一种。您可以在这里找到它:(请原谅德国的评论)
像这样使用它:
GFStringBuilder.format("Your score is: % and your name is %").eat(score).eat(name).result
所有内容都写入一个char[]
数组中。我必须手动实现从整数到字符串的转换(逐位)以获得
GFStringBuilder.format("Your score is: % and your name is %").eat(score).eat(name).result
for(GameObject gob : interactiveObjects)
gob.onDraw(canvas);
for (int i = 0; i < interactiveObjects.size(); i++) {
interactiveObjects.get(i).onDraw();
}