如何避免 Java 游戏中的垃圾回收延迟?(最佳实践)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/2484079/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How can I avoid garbage collection delays in Java games? (Best Practices)
提问by Brian
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.
我正在为 Android 平台在 Java 中调整交互式游戏的性能。有时,垃圾收集的绘图和交互会出现问题。通常它不到十分之一秒,但有时在非常慢的设备上它可以达到 200 毫秒。
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.
我正在使用 ddms 分析器(Android SDK 的一部分)来搜索我的内存分配来自哪里,并从我的内部绘图和逻辑循环中删除它们。
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.
每次执行循环时都会iterator
分配一个。我现在ArrayList
为我的对象使用数组 ( )。如果我想在内循环中使用树或散列,我知道我需要小心甚至重新实现它们,而不是使用 Java Collections 框架,因为我负担不起额外的垃圾收集。当我查看优先队列时可能会出现这种情况。
I also have trouble where I want to display scores and progress using Canvas.drawText
. This is bad,
我也想使用Canvas.drawText
. 这不好,
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.
因为Strings
,char
数组和StringBuffers
将被全部分配以使其工作。如果您有几个文本显示项目并且每秒运行 60 次框架,则该框架开始累加并且会增加您的垃圾收集问题。我认为这里最好的选择是保留char[]
数组并将您的int
或double
手动解码到它并将字符串连接到开头和结尾。我想听听是否有更清洁的东西。
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 或 Android 上交互式运行的陷阱和最佳实践是什么?这些gc问题足以让我想念手动内存管理,但不是很多。
采纳答案by SyntaxT3rr0r
I've worked on Java mobile games... The best way to avoid GC'ing objects (which in turn shalltrigger the GC at one point or another and shallkill your game's perfs) is simply to avoid creating them in your main game loop in the first place.
我对Java的手机游戏合作......以避免GC'ing对象(这反过来,最好的办法应在一个点触发GC或其他和必杀死你的游戏的perfs)仅仅是为了避免在主游戏创建它们首先循环。
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):
通常,您在屏幕上有 4 个球,分别位于 (50,25)、(70,32)、(16,18)、(98,73)。好吧,这是你的抽象(为了这个例子而简化):
n = 4;
int[] { 50, 25, 70, 32, 16, 18, 98, 73 }
You "pop" the 2nd ball which disappears, your int[] becomes:
你“弹出”消失的第二个球,你的 int[] 变成:
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).
(请注意,我们甚至不关心“清理”第 4 个球 (98,73),我们只是跟踪剩下的球数)。
Manual tracking of objects, sadly. This how it's done on most current well-performing Java games that are out on mobile devices.
遗憾的是,手动跟踪对象。这是如何在移动设备上运行的大多数当前性能良好的 Java 游戏中完成的。
Now for strings, here's what I'd do:
现在对于字符串,这就是我要做的:
- at game initialization, predraw using drawText(...)only oncethe 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(...)预绘制一次您保存在
BufferedImage[10]
数组中的数字 0 到 9 。 - 在游戏初始化时,预绘制一次“你的分数是:”
- 如果“你的分数是:”真的需要重绘(因为,比如说,它是透明的),那么从你预先存储的
BufferedImage
- 循环计算分数的数字并在“您的分数是:”之后手动添加每个数字(通过每次从您
BufferedImage[10]
预先存储它们的位置复制相应的数字(0 到 9)。
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 alsododged the call to drawtext(...)which itself mayvery well be crappily generating, well, needless crap).
这为您提供了两全其美的方法:您可以重用drawtext(...)字体,并且在主循环期间创建了恰好零个对象(因为您还避免了对drawtext(...)的调用,它本身很可能是糟糕地产生,好吧,不必要的废话)。
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).
这不是“干净”,也不是“好的做法”,但这就是一流手机游戏(例如 Uniwar)中的做法。
And it's fast. Darn fast. Faster than anythinginvolving 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).
PS:其实仔细看几款手游,你会发现很多时候字体其实不是系统/Java字体,而是专为每款游戏制作的像素完美的字体(这里我只是举了一个例子来说明如何缓存系统/Java 字体,但显然您也可以缓存/重用像素完美/位图字体)。
回答by gustafc
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:
如果您不想按照建议的方式预渲染文本,请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);
Now drawing the score doesn't cause any allocations (except maybe once or twice in the beginning when buf
s internal array may have to be grown, and whatever drawText
is up to).
现在绘制分数不会导致任何分配(除非在开始时buf
可能需要增加 s 内部数组的一两次,以及其他任何drawText
情况)。
回答by Stephen C
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.
在避免 GC 暂停至关重要的情况下,您可以使用的一个技巧是在您知道暂停无关紧要的时候故意触发 GC。例如,如果在游戏结束时使用垃圾密集型“showScores”功能,用户不会因显示得分屏幕和开始下一场比赛之间额外的 200 毫秒延迟而过度分心......所以你可以调用System.gc()
绘制分数屏幕后。
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.
但是如果你使用这个技巧,你需要小心地只在 GC 暂停不会令人讨厌的地方这样做。如果您担心耗尽手机的电池,请不要这样做。
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.
并且不要在多用户或非交互式应用程序中执行此操作,因为这样做很可能会使应用程序整体运行速度变慢。
回答by David Scherfgen
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).
我构建了自己的无垃圾版本String.format
,至少是一种。你可以在这里找到它:http: //pastebin.com/s6ZKa3mJ(请原谅德国人的评论)。
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.
一切都被写入一个char[]
数组。我必须手动实现从整数到字符串的转换(逐位)以摆脱所有垃圾。
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.
除此之外,我用SparseArray
在可能的情况,因为所有的Java数据结构等HashMap
,ArrayList
等,必须使用拳击,以应对基本类型。每次将int
to装箱时Integer
,Integer
GC 都必须清除此对象。
回答by Eonil
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.
避免 GC 延迟的唯一也是最好的方法是通过静态分配所有必需的对象(包括在启动时)来避免 GC 本身。预先创建所有需要的对象,不要让它们被删除。使用对象池来重用现有对象。
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 internallywhich will become garbage eventually. For example, Java base library. Even using of simple List
class maycreate garbages. (so should be avoided) Calling any of Java API may create garbages. And these allocations are not avoidable while you're using Java.
无论如何,即使在对代码进行了所有可能的优化之后,您也可能最终会暂停。因为除了您的应用程序代码之外的任何其他内容仍在内部创建 GC 对象,最终将成为垃圾。例如,Java 基础库. 即使使用简单的List
类也可能会产生垃圾。(所以应该避免)调用任何 Java API 可能会产生垃圾。当您使用 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 allowsthe GC, all the libraries mayuse GC, so you virtually/practically have no library. I regard avoiding GC on GC based language is a kind of insane trial.
此外,因为 Java 旨在利用 GC,如果您真的试图避免 GC,您将因缺乏功能而遇到麻烦。(甚至List
应该避免使用类)因为它允许GC,所以所有的库都可以使用 GC,所以你实际上/实际上没有 library。我认为在基于 GC 的语言上避免 GC 是一种疯狂的尝试。
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.
归根结底,唯一可行的方法是下降到可以完全控制自己记忆的较低级别。如C族语言(C、C++等)。所以去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.
现在 Google 正在推出增量(并发?)GC,这可以大大减少暂停。无论如何,增量 GC 意味着只是随时间分配 GC 负载,因此如果分配不理想,您仍然会看到最终的暂停。此外,由于批处理和分发操作开销较少的副作用,GC 性能本身也会降低。
回答by user1410657
Regarding the iterator allocation, avoiding iterators on ArrayList`s is easy. Instead of
关于迭代器分配,避免 ArrayList 上的迭代器很容易。代替
for(GameObject gob : interactiveObjects)
gob.onDraw(canvas);
you can just do
你可以做
for (int i = 0; i < interactiveObjects.size(); i++) {
interactiveObjects.get(i).onDraw();
}