Сборщик мусора не освобождает «трэш-память», как должно быть в приложении Android - PullRequest
0 голосов
/ 05 сентября 2018

Hello!

Я начинающий Java и Android-разработчик, и у меня в последнее время возникают проблемы с управлением памятью моего приложения. Я разбью этот текст на разделы, чтобы сделать его более понятным и читабельным.


Краткое описание моего приложения

Это игра, которая состоит из нескольких этапов (уровней). Каждый этап имеет начальную точку для игрока и выход, который ведет игрока к следующему этапу. Каждый этап имеет свой набор препятствий. В настоящее время, когда игрок достигает финальной стадии (я только создал 4), он / она автоматически возвращается к первой стадии (уровень 1).

Абстрактный класс GameObject (расширяет Android.View ) определяет базовую структуру и поведение игрока и всех других объектов (препятствий и т. Д.), Присутствующих в игре. Все объекты (которые, по сути, являются представлениями) отрисовываются в созданном мной пользовательском представлении (расширяет FrameLayout) Логика игры и игровой цикл обрабатываются сторонним потоком (gameThread). Этапы создаются путем извлечения метаданных из XML-файлов.

проблема

Помимо всех возможных утечек памяти в моем коде (все из которых я усердно трудился, чтобы найти и решить), существует странное явление, связанное с сборщиком мусора. Вместо того, чтобы описать это словами и рискнуть запутать вас, я буду использовать изображения. Как сказал Конфуций: «Изображение стоит тысячи слов». Ну, в данном случае, я только что спас вас от чтения 150 000 слов, поскольку мой GIF ниже имеет 150 кадров.

Stage 1 when firstly loaded. The total memory allocation of the app is 85mb. Stage 1 when loaded for the second time. The total memory allocation of the app is 130mb. After I forcefully perform 2 garbage collections (using Android Profiler), the memory goes back to 85mb.

Описание: первое изображение представляет использование памяти моим приложением при первой загрузке «стадии 1». Второе изображение (GIF) сначала представляет временную шкалу использования памяти моего приложения, когда «этап 1» загружается во второй раз (это происходит, как описано ранее, когда проигрыватель прошел последний этап), и за ним следует четыре мусора коллекции , насильно инициированные мной.

Как вы могли заметить, между двумя ситуациями существует огромная разница (почти 50 МБ) в использовании памяти. Когда «Стадия 1» загружается впервые, когда запускается игра, приложение использует 85 МБ памяти. Когда один и тот же этап загружается во второй раз, чуть позже, использование памяти уже составляет 130 МБ! Это, вероятно, из-за плохого кодирования с моей стороны, и я не здесь из-за этого. Вы заметили, как после того, как я принудительно выполнил 2 (фактически 4, но только первые 2 имели значение) сборки мусора, использование памяти вернулось в свое «нормальное состояние» (такое же использование памяти, как при первой загрузке сцены)? Это странное явление, о котором я говорил .

Вопрос

Если сборщик мусора должен удалить из памяти объекты, на которые больше не ссылаются (или, по крайней мере, имеют только слабые ссылки ), почему «мусорная память», которую вы видели выше, удаляется только тогда, когда я принудительно вызываю GC , а не на GC нормальных казнях? Я имею в виду, если сборщик мусора , инициированный мной вручную, может удалить этот "трэш", тогда обычные GC's казнь также смогут удалить его. Почему этого не происходит?

Я даже пытался вызвать System.gc () , когда переключаются этапы, но, несмотря на то, что сборка мусора происходит, эта «трэш» память не удаляется как когда я вручную выполняю GC . Я что-то упускаю из-за того, как работает сборщик мусора или как это реализует Android?

Заключительные замечания

Я потратил дни на поиск, изучение и внесение изменений в мой код, но я не мог понять, почему это происходит. StackOverflow - это мое последнее средство. Спасибо!

ПРИМЕЧАНИЕ: Я собирался опубликовать некоторую, возможно, соответствующую часть исходного кода моего приложения, но, поскольку вопрос уже слишком длинный, я остановлюсь здесь. Если вы чувствуете необходимость проверить код, просто дайте мне знать, и я отредактирую этот вопрос.

Что я уже прочитал:
Как форсировать сборку мусора в Java?
Сборщик мусора в Android
Основы сборки мусора Java от Oracle
Обзор памяти Android
Шаблоны утечки памяти в Android
Предотвращение утечек памяти в Android
Управление памятью вашего приложения
Что нужно знать об утечках памяти приложения Android
Просмотр кучи Java и выделения памяти с помощью Memory Profiler
LeakCanary (библиотека обнаружения утечек памяти для Android и Java)
Android утечка памяти и сборка мусора
Общая сборка мусора для Android
Как очистить динамически создаваемый вид из памяти?
Как работают ссылки в Android и Java
Java Garbage Collector - Не работает нормально с регулярными интервалами
Сборка мусора в андроиде (сделано вручную)
... и больше я не смог найти снова.

1 Ответ

0 голосов
/ 07 сентября 2018

Сборка мусора сложна, и разные платформы реализуют ее по-разному. Действительно, разные версии одной и той же платформы осуществляют сборку мусора по-разному. (И еще ...)

Типичный современный коллекционер основан на наблюдении, что большинство объектов умирают молодыми; то есть они становятся недоступными вскоре после их создания. Затем куча делится на два или более «пробелов»; например "молодое" пространство и "старое" пространство.

  • «Молодое» пространство - это место, где создаются новые объекты, и оно часто собирается. «Молодое» пространство имеет тенденцию быть меньше, и «молодая» коллекция происходит быстро.
  • «Старое» пространство - это место, где заканчиваются долгоживущие объекты, и оно собирается нечасто. На «старом» пространстве коллекция, как правило, обходится дороже. (По разным причинам.)
  • Объект, переживший несколько циклов GC в «новом» пространстве, получает «владение»; то есть они перемещены в "старое" пространство.
  • Иногда мы можем обнаружить, что нам нужно собирать новые и старые пробелы одновременно. Это называется полная коллекция. Полный сборщик мусора является самым дорогим и, как правило, «останавливает мир» на относительно долгое время.

(Есть много других умных и сложных вещей ... которые я не буду вдаваться.)


Ваш вопрос заключается в том, почему использование пространства не значительно снижается, пока вы не позвоните System.gc().

Ответ в основном заключается в том, что это эффективный способ сделать что-то.

Настоящая цель сбора не состоит в том, чтобы постоянно освобождать столько памяти. Скорее, цель состоит в том, чтобы обеспечить достаточное количество свободной памяти, когда это необходимо, и сделать это либо с минимальными перегрузками ЦП, либо с минимумом пауз GC.

Таким образом, при нормальной работе ГХ будет вести себя так, как указано выше: делать частые "новые" коллекции пространств и реже "старые" коллекции пространств. И коллекции будет работать "как требуется".

Но когда вы звоните System.gc(), JVM , как правило, пытается вернуть как можно больше памяти. Это означает, что он делает "полный gc".

Теперь я думаю, что вы сказали, что для того, чтобы реально изменить ситуацию, требуется пара System.gc() вызовов, которые могут быть связаны с использованием finalize методов или Reference объектов или подобных объектов. Оказывается, что финализуемые объекты и Reference обрабатываются после того, как основной GC завершен фоновым потоком. На самом деле объекты находятся только в состоянии, когда их можно собирать и удалять после этого. Так что нужен еще один GC, чтобы окончательно избавиться от них.

Наконец, существует проблема общего размера кучи. Большинство виртуальных машин запрашивают память у операционной системы хоста, когда куча слишком мала, но не хотят ее возвращать. Причина в том, что сборщики, как правило, наиболее эффективны, если пропорция мусора к не-мусору высока. Возвращая освобожденную память обратно ОС, JVM повлияет на производительность позже ... если куча снова вырастет.

Я не уверен насчет сборщика Android, но сборщики Oracle отмечают соотношение свободного пространства в конце последовательных "полных" сборок. Они уменьшают общий размер кучи только в том случае, если коэффициент свободного пространства «слишком высок» после заданного числа циклов.

Предполагая, что сборщик Android работает аналогично, это еще одно объяснение того, почему вам пришлось запускать System.gc() несколько раз, чтобы уменьшить размер кучи.

...