【问题标题】:How can I avoid garbage collection delays in Java games? (Best Practices) [closed]如何避免 Java 游戏中的垃圾收集延迟? (最佳实践)[关闭]
【发布时间】:2011-01-29 20:33:31
【问题描述】:

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

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

最严重的违规行为是像这样的短循环,

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 次框架,那么就会开始累加,并会增加您的垃圾收集打嗝。我认为这里最好的选择是保留char[] 数组并手动将intdouble 解码到其中并将字符串连接到开头和结尾。我想知道是否有更清洁的东西。

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

【问题讨论】:

标签: java android garbage-collection performance


【解决方案1】:

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

像这样使用它:

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

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

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

【讨论】:

  • 你有没有把你的版本放到 GitHub 上的某个地方来分享? (或者你只是在吹牛?)
  • Here it is ... 请注意,目前它只支持整数和字符串,没有格式化选项,因为到目前为止我还不需要它们。以后会补充更多。请用德语原谅 cmets :)
【解决方案2】:

虽然这是一个 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 性能本身也会降低。

【讨论】:

    【解决方案3】:

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

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

    你可以这样做

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

      • 这并不能保证 GC 将按照您的要求运行?它可以在 10 毫秒到 1 分钟后的任何时间运行(显然不确定精确的测量值)。但无论哪种方式,这都不是一个很好的解决方案,因为您可能会调用 GC 来运行它,它会在游戏开始后运行。
      • @While-E:您拥有的一个保证是,当System.gc() 返回时,集合已经运行或没有运行。我的意思是,System.gc() 根本不能保证做任何事情......但如果它做了,无论它做什么都将在函数返回之前完成。 (根据the docs,“当控制从方法调用返回时,Java 虚拟机已尽最大努力从所有丢弃的对象中回收空间。”)例如,它不会产生 GC 线程并立即返回。
      • @cHao:我没有意识到这一点,谢谢你对我的启发!
      【解决方案5】:

      如果您不想按照建议预渲染文本,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);
      

      现在绘制分数不会导致任何分配(除非在开始时可能需要增加一次或两次 bufs 内部数组,以及 drawText 的任何操作)。

      【讨论】:

      • 当你调用buf.append(value)时,它会导致String对象的分配。 append 方法通过String.valueOf(int)int 值转换为String
      • String.format(...) 的性能如何?它只分配一个字符串吗??
      【解决方案6】:

      我从事过 Java 手机游戏...避免 GC'ing 对象的最佳方法(反过来 shall 在某一时刻触发 GC 并且 shall em> kill your game's perfs) 只是为了避免一开始就在主游戏循环中创建它们。

      没有“干净”的方法来处理这个问题,我先举个例子......

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

      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 字体的示例,但显然您也可以缓存/重用像素完美/位图字体)。

      【讨论】:

      • 幸运的是,Android 开发人员的图形代码位于 Skia 本机库中,而不是 Java 的轻量级框架中。结果是我们可以将各种转换和缓冲区甚至文本扔到屏幕上,而不必过多担心内存和性能。当您需要为手动抗锯齿或手动计算转换矩阵分配像素缓冲区时,问题就出现了;为此,我有手动跟踪对象的缓存。我认为,Strings 和 StringBuffers 需要相同的处理,而循环计算数字是我的下一个目标。感谢您提供详细信息。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-16
      • 2017-04-07
      • 2013-08-24
      • 2015-08-02
      • 1970-01-01
      • 2016-03-11
      相关资源
      最近更新 更多