Подтвердить что ты не робот

Как я могу избежать задержек сбора мусора в Java-играх? (Лучшие практики)

Я настраиваю интерактивные игры на 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);

потому что Strings, char массивы и StringBuffers будут распределены по всему, чтобы они работали. Если у вас есть несколько элементов отображения текста и запустите фрейм 60 раз в секунду, который начинает складываться и увеличит ваши икоты сбора мусора. Я думаю, что лучший выбор здесь - сохранить массивы char[] и декодировать ваши int или double вручную в него и объединить строки в начало и конец. Я хотел бы услышать, если там что-то более чистое.

Я знаю, что там должны быть другие, которые занимаются этим. Как вы справляетесь с этим, и каковы подводные камни и лучшие практики, которые вы обнаружили для интерактивного запуска на Java или Android? Эти проблемы с gc достаточно, чтобы я пропустил ручное управление памятью, но не очень.

4b9b3361

Ответ 1

Я работал над Java-мобильными играми... Лучший способ избежать объектов GC (которые, в свою очередь, будут запускать GC в тот или иной момент и убьют ваши игровые перфомансы), - это просто не создавать их в ваш основной игровой цикл в первую очередь.

Нет никакого "чистого" способа справиться с этим, и я сначала приведу пример...

Обычно у вас есть, скажем, 4 мяча на экране (50,25), (70,32), (16,18), (98,73). Ну, вот ваша абстракция (упрощенная ради этого примера):

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

Вы "поп" 2-го шара, который исчезает, ваш int [] становится:

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

(обратите внимание на то, что мы даже не заботимся о "чистке" 4-го шара (98,73), мы просто отслеживаем количество оставшихся мячей).

Ручное отслеживание объектов, к сожалению. Это было сделано в большинстве современных хорошо работающих Java-игр, которые находятся на мобильных устройствах.

Теперь для строк, вот что я сделал бы:

  • при инициализации игры, predraw используя drawText (...) только один раз с номерами от 0 до 9, которые вы сохраняете в массиве BufferedImage[10].
  • при инициализации игры, predraw once "Ваша оценка:"
  • если "Ваша оценка:" действительно нужно перерисовать (потому что, скажем, она прозрачна), а затем перерисовать ее из предварительно сохраненного BufferedImage
  • чтобы вычислить цифры оценки и добавить, после "Ваша оценка:" , каждая цифра вручную по одному (путем копирования каждого времени соответствующей цифры (от 0 до 9) из вашего BufferedImage[10], где вы предварительно сохраненные.

Это дает вам лучшее из мира: вы повторно используете шрифт drawtext (...), и вы создали ровно нулевые объекты во время основного цикла (потому что вы также избежали вызова drawtext (...), который сам может очень хорошо быть crappily генерации, ну, ненужное дерьмо).

Другим "преимуществом" этого "нуля" создания нулевого объекта является то, что тщательное кэширование и повторное использование изображений для шрифтов на самом деле не "ручное распределение/освобождение объекта", это действительно просто тщательное кэширование.

Это не "чистый", это не "хорошая практика", а то, как это делается в первоклассных мобильных играх (например, Uniwar).

И это быстро. Дерьмо быстро. Быстрее, чем что-либо, связанное с созданием объекта.

PS: На самом деле, если внимательно посмотреть на несколько мобильных игр, вы заметите, что часто шрифты на самом деле не являются системными/Java-шрифтами, а пиксельными идеальными шрифтами, созданными специально для каждой игры (здесь я просто привел пример того, как для кэширования системного/Java-шрифта, но, очевидно, вы также можете кэшировать/повторно использовать пиксель-идеальный/растровый шрифт).

Ответ 2

Хотя это вопрос 2 года...

Единственный и лучший подход, чтобы избежать отставания GC, заключается в том, чтобы избежать самой GC, выделив все требуемые объекты несколько статически (в том числе при запуске). Предварительно создайте все необходимые объекты и никогда не удаляйте их. Используйте объединение объектов для повторного использования существующего объекта.

В любом случае у вас может быть возможная пауза даже после всех возможных оптимизаций вашего кода. Поскольку ничего, кроме вашего кода приложения, по-прежнему создает объекты GC внутри, которые в конечном итоге станут мусором. Например, базовая библиотека Java. Даже использование простого класса List может создавать мусор. (так что этого следует избегать). Вызов любого из API Java может создавать мусор. И эти распределения нельзя избежать, пока вы используете Java.

Кроме того, поскольку Java предназначен для использования GC, у вас возникнут проблемы из-за отсутствия функций, если вы действительно попытаетесь избежать GC. (следует избегать класса List). Поскольку он позволяет GC, все библиотеки могут использовать GC, поэтому вы практически/практически не имеете библиотеки. Я считаю, что избегать GC на языке, основанной на GC, является своего рода безумным испытанием.

В конечном счете, единственный практический способ - спуститься на более низкий уровень, где вы можете полностью контролировать память. Такие, как семейные языки C (C, С++ и т.д.). Поэтому перейдите в NDK.

Примечание

Теперь Google отправляет инкрементный (параллельный?) GC, который может значительно уменьшить паузу. В любом случае инкрементный GC означает просто распределение нагрузки GC во времени, поэтому вы все равно видите возможную паузу, если распределение не является идеальным. Также сама производительность GC будет уменьшена из-за побочного эффекта меньших затрат на доработку и распределение.

Ответ 3

Я построил собственную версию без мусора String.format, по крайней мере, отчасти. Вы найдете его здесь: http://pastebin.com/s6ZKa3mJ (пожалуйста, извините немецкие комментарии).

Используйте его следующим образом:

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

Все записано в массив char[]. Мне пришлось реализовать преобразование из целого числа в строку вручную (по цифре), чтобы избавиться от всего мусора.

Кроме того, я использую SparseArray, где это возможно, потому что все структуры данных Java, такие как HashMap, ArrayList и т.д., должны использовать бокс, чтобы иметь дело с примитивными типами. Каждый раз, когда вы вставляете int в Integer, этот объект Integer должен быть очищен GC.

Ответ 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 до).

Ответ 5

В ситуации, когда крайне важно избегать пауз GC, один трюк, который вы можете использовать, - это намеренно запустить GC в точке, где вы знаете, что пауза не имеет значения. Например, если в конце игры используется интенсивная функция showScores для мусора, пользователь не будет слишком отвлекаться дополнительной задержкой в ​​200 мс между отображением экрана счета и началом следующей игры... так что вы могли бы позвонить System.gc() после того, как будет нарисован экран оценок.

Но если вы прибегаете к этому трюку, вам нужно быть осторожным, чтобы делать это только в тех точках, где пауза GC не будет раздражать. И не делайте этого, если вы беспокоитесь о том, чтобы разрядить батарею телефона.

И не делайте этого в многопользовательских или неинтерактивных приложениях, потому что вы, скорее всего, сделаете это приложение медленнее, выполнив это.

Ответ 6

Что касается распределения итератора, то избегать итераторов на ArrayList легко. Вместо

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

вы можете просто сделать

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