Обзор сборки мусора в Java

Сборка мусора в Java


Декабрь 15, 2022


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

В этой статье мы расскажем, как устроена сборка мусора в Java, какие бывают сборщики, и какой из них выбрать для вашего приложения. Вы также узнаете, как правильно настраивать GC для достижения максимальной производительности по выбранным показателям.

  1. Как работает автоматическая сборка мусора
    1. Общий принцип
    2. Преимущества автоматической сборки мусора
    3. Недостатки
  2. Какие сборщики мусора есть в Java
    1. Serial GC
    2. Parallel GC
    3. Concurrent Mark Sweep GC
    4. G1 GC
    5. Z GC
    6. Shenandoah GC
  3. Сборка мусора в нативных образах
  4. Лучшие практики сборки мусора
  5. Заключение

Как работает автоматическая сборка мусора

Общий принцип

Сборка мусора (GC) — это процесс освобождения памяти путем удаления неиспользуемых объектов из кучи. Объект подлежит удалению, если он нигде не используется, т.е. на него больше нет ссылок. В отличие от C/C++, где за уничтожение объектов отвечает разработчик, сборка мусора в Java работает автоматически. Сборку мусора можно запустить вручную, вызвав System.gc(), но так делать не рекомендуется. System.gc() может спровоцировать полный цикл GC, когда куча очищается полностью. Метод будет ждать возможности выполнить полный цикл, а это отразится на производительности программы.

Прежде, чем подробно рассматривать разные сборщики мусора в Java, следует разобраться в механизме GС.

Одна из функций Java GC — управление поколениями объектов. Новые объекты относятся к молодому поколению (young generation). Как правило, большинство объектов умирают «в молодости», но некоторые переходят в старшее поколение (old generation). Все поколения занимают разные области кучи JVM:

  • Метапространство (Metaspace), заменившее PermGen в версиях Java 8+, где хранятся метаданные;
  • Эдем (Eden) и две области Выживших (Survivor), 0 и 1, где находятся объекты молодого поколения;
  • Хранилище (Tenured), куда помещаются объекты старшего поколения.

Когда любая из этих областей переполняется, происходит сборка мусора. Малая сборка (minor collection) происходит при заполнении областей молодого поколения. Большинство молодых объектов умирают и подлежат удалению, но если какие-то из них все еще используются на момент GC, они перемещаются в область Выживших 0 или 1, а оттуда в Хранилище. При заполнении Хранилища происходит основная сборка (major collection). Основная сборка обычно занимает больше времени из-за большего количества объектов.

Теорию разобрали, что происходит на практике?

При запуске приложения область Эдема пуста. В процессе работы потоков куча заполняется, после чего событие — переполнение области — запускает сборку мусора. В случае использования непараллельных сборщиков мусора потоки останавливаются и ждут, пока сборщик завершит работу. Сборщик помечает неиспользуемые объекты, затем удаляет их. Другие события, способные запустить GC:

  • Недостаточно памяти для создания новых объектов;
  • Явный вызов GC.

После очистки памяти потоки возобновляют работу. Паузы, возникающие во время сборки мусора, называются Stop-the-World. Оптимальная производительность приложения достигается за счет уменьшения количества и продолжительности пауз.

Преимущества автоматической сборки мусора

  • Нет необходимости в ручном распределении и очистке памяти, что экономит время разработчиков и снижает риск ошибок.
  • Эффективное использование памяти, так как куча освобождается сразу по заполнении.
  • Снижение риска утечки памяти — в большинстве случаев сборщик мусора не допускает подобных ситуаций.

Недостатки

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

Какие сборщики мусора есть в Java

В Java сборку мусора можно регулировать. Существует несколько сборщиков, каждый со своими преимуществами. Сперва компания определяет ключевые показатели производительности: пропускную способность, время отклика и потребление памяти. Выбор сборщика мусора зависит от поставленных целей.

Например, если важна высокая пропускная способность, следует выбрать параллельный сборщик (Parallel GC). G1 GC помогает улучшить время отклика. При этом любой выбранный сборщик мусора можно настроить. Настройка GC — почти ювелирная работа: при большой куче паузы будут длиннее, короткие паузы связаны с частым вызовом GC и так далее.

Ниже представлен обзор основных сборщиков мусора в JVM и их характеристики.

Serial GC

Серийный сборщик мусора (Serial GC) — старейший и простейший сборщик в Java. Он используется в однопоточных приложениях, так как замораживает все потоки во время сборки мусора и сам работает в одном потоке. Serial GC подходит для клиентских приложений, которым не важны короткие паузы. Этот GC будет использован автоматически, если установить предел RAM меньше 1792 MB, или если доступен только один CPU. В остальных случаях его можно активировать командой

java -XX:+UseSerialGC -jar yourApp.java

Parallel GC

Параллельный сборщик мусора (Parallel GC) подходит для многопроцессорных систем. Он тоже замораживает все потоки, но, в отличие от Serial GC, использует несколько потоков для ускорения очистки памяти. Разработчик может установить максимальное количество потоков GC. Например, команда

java -XX:+UseParallelGC -XX:ParallelGCThreads=10 -jar yourApp.java

активирует Parallel GC с десятью потоками. В реальных условиях количество потоков зависит от количества процессоров. Parallel GC использует несколько потоков для удаления объектов молодого поколения, но обходится одним при работе со старшим поколением. В новых версиях Java можно включить Parallel Old GC (-XX:+UseParallelOldGC), использующий несколько потоков с обоими поколениями. Кроме того, у всех сборщиков кроме Serial GC можно устанавливать время паузы (-XX:MaxGCPauseMillis) для поддержания оптимальной пропускной способности. Но не устанавливайте слишком низкое значение, иначе JVM будет использовать маленькую кучу для быстрой сборки мусора, и количество пауз увеличится. Другой флаг, -XX:GCTimePercentage, позволяет разработчикам устанавливать время, которое приложение тратит на сборку мусора.

Concurrent Mark Sweep GC

У CMS GC две отличительные черты:

  • Использует несколько потоков для сборки мусора;
  • Делит ресурсы процессора с приложением, т.е. не замораживает потоки, а использует некоторые из них для GC.

Этот сборщик подходит для приложений, которым необходимы короткие паузы и которые могут позволить делить ресурсы с GC. CMS GC работает медленнее, чем Parallel GC или Serial GC, но зато не останавливает приложение. Так как он выполняет сборку мусора в многопоточном режиме, вызов System.gc()приведет к ошибке. Для активации CMS GC выполните команду

java -XX:+UseConcMarkSweepGC -jar yourApp.java

Важно отметить, что, начиная с Java 9, CMS GC помечен как устаревший с целью «ускорения разработки других сборщиков в HotSpot» (JEP 291), поэтому при попытке включить этот GC в Java 11 возникнет следующее предупреждение:

java -XX:+UseConcMarkSweepGC --version
OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.

В более новых версиях Java он был полностью удален, поэтому Java 17 выдаст следующее сообщение:

Unrecognized VM option 'UseConcMarkSweepGC'

G1 GC

G1 GC был разработан для замены CMS GC с целью улучшения времени отклика. Он подходит для любого приложения, но наибольшую пользу от него получат серверные приложения, работающие в многопроцессорной среде с большими кучами (6+ GB). Этот сборщик разделяет кучу на множество областей различного размера, от 1 MB до 32 MB, и выполняет глобальную маркировку объектов. Определив, какие области наиболее пустые, он сначала выполняет сборку мусора там, освобождая большой объем памяти. Такой подход называется Garbage-First.

G1 GC копирует объекты из нескольких областей памяти в один, таким образом уплотняя занятую память. Уплотнение выполняется параллельно в многопроцессорных системах, что позволяет сократить паузы и увеличить пропускную способность. Кроме того, разработчики могут регулировать максимальное время паузы и интервалы пауз.

Чтобы включить G1 GC, выполните команду

java -XX:+UseG1GC -jar yourApp.java

Z GC

Z GC был включен в Java 11 в качестве экспериментальной функции. Начиная с Java 15, он может использоваться в проде. Это масштабируемый сборщик с низким временем отклика, выполняющий сборку в режиме многопоточности и не останавливающий потоки приложения больше, чем на 10 мс. Важнейший параметр — максимальный размер кучи (-Xmx<size>): в ней должны помещаться все живые объекты и еще оставаться место для выделения памяти. Для использования X GC выполните

java -XX:+UseZGC -jar yourApp.java

Для версий старше JDK 15 команда будет немного отличаться:

java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar yourApp.java

Shenandoah GC

Еще один сборщик мусора с короткими паузами — Shenandoah GC. Он выполняет сборку параллельно с работающим Java приложением, сокращая паузы, которые не связаны с размером кучи напрямую. Oracle Java не включает Shenandoah, но этот сборщик поддерживается основными дистрибутивами OpenJDK, включая Axiom JDK. Shenandoah входит в состав всех LTS релизов и текущий релиз Java и совместим с большинством платформ. Сообщество OpenJDK бэкпортирует улучшения и фиксы в предыдущие версии JDK.

Для включения Shenandoah GC используйте команду

java -XX:+UseShenandoahGC -jar yourApp.java

Сборка мусора в нативных образах

Как происходит управление памятью в нативных образах?

Нативные образы используют в качестве виртуальной машины не HotSpot, а GraalVM. Хотя механизм GC такой же, набор сборщиков мусора отличается:

  • Serial GC — дефолтный сборщик в обеих версиях GraalVM Community и Enterprise Edition. Он способствует низкому потреблению памяти и минимальному оверхеду за счет увеличенного времени отклика. Максимальный размер кучи при использовании Serial GC автоматически устанавливается на 80% доступной физической памяти, если размер кучи не указывают явно посредством флагов -Xmx или -XX:MaximumHeapSizePercent.
  • G1 GC доступен только в версии GraalVM EE. Он обеспечивает оптимальный баланс между временем отклика и пропускной способностью. По умолчанию максимальный размер кучи устанавливается на 25% физической памяти, но этот параметр можно изменить. Также можно настроить время паузы, количество потоков и т.д.
  • Epsilon GC поддерживается в версиях GraalVM 21.2+. Он не собирает мусор и предназначен для недолго работающих приложений с небольшими объемами памяти.

Более подробная информация о настройке GC в нативных образах содержится в официальной документации GraalVM.

Axiom NIK — утилита на базе GraalVM для генерации нативных образов и ускорения запуска приложений — поддерживает все эти сборщики мусора. Axiom NIK — продукт с российской техподдержкой от лицензиата ФСТЭК России и лидера OpenJDK. Приобретая лицензию на Axiom NIK, вы получаете доступ к последним версиям с патчами безопасности и другими улучшениями.

Лучшие практики сборки мусора

Настройка сборки мусора требует времени, так как универсального решения нет — все зависит от приложения, среды, в которой оно работает, и ключевых показателей производительности. Мы уже говорили о важности настройки GC в статье о конфигурации HotSpot. Сейчас мы хотели бы дать несколько общих рекомендаций по управлению сборкой мусора в Java:

  • Не меняйте настройки GC без необходимости. Маленькие приложения, не работающие с большими объемами памяти, прекрасно обходятся автоматической сборкой мусора. Но даже в случае больших энтерпрайз-приложений сначала убедитесь, что низкая производительность связана со стандартными настройками GC. Иногда достаточно переключиться на другой сборщик, чьи настройки по умолчанию будут более оптимальны для вашего приложения.
  • Производительность измеряется тремя основными показателями: пропускная способность, время отклика и потребление памяти. Выберите два и настраивайте GC, ориентируясь на них, так как улучшение двух показателей обычно идет в ущерб третьему. Например, чем больше памяти вы выделяете приложению, тем лучше пропускная способность, но при этом паузы длиннее. С другой стороны, чем меньше куча, тем меньше время отклика, но частые паузы негативно сказываются на пропускной способности.
  • Для корректной настройки GC важно понимать поведение JVM и что происходит в куче. Используйте средства мониторинга Java, такие как JFR, Mission Control, jstats или другие профайлеры для сборки метрик о потреблении памяти, сборке мусора и производительности в целом.
  • Освойте логирование GC, так как логи содержат подробную информацию о поведении сборщика, очистке и распределении памяти, продолжительности пауз и т.д. Для включения логов GC используйте команду

      -Xlog:gc*:<gc.log file path>:time
    

    Рекомендуется хранить логи в отдельных файлах, чтобы они не смешивались с логами приложения.

  • Следует избегать вызова System.gc(), но вы можете помочь сборщику по-другому: не ссылайтесь на неиспользуемые объекты, чтобы сборщик мог их удалить. Например, можно обнулить или переназначить ссылочные переменные.
  • Иногда для решения проблемы достаточно выставить минимальный (-Xms) и максимальный (-Xmx) размер кучи. В некоторых случаях требуется более тонкая настройка с тщательным мониторингом и бенчмаркингом. Но чем сильнее вы меняете настройки GC, тем ненадежнее результат. При малейших изменениях среды, будь то обновление оборудования, масштабирование приложения и пр., вам придется снова повторить весь процесс настройки GC.
  • Настройка GC не улучшит производительность приложения как по волшебству. Если после всех изменений вы недовольно результатом, стоит задуматься об альтернативных решениях: смене оборудования, обновлении ПО или рефакторинге кода.

Заключение

Мы описали механизм автоматической сборки мусора в Java и рассмотрели разные сборщики мусора. У каждого из них своих особенности и преимущества, поэтому вы можете выбрать наиболее подходящий для вашего приложения.

Так как Axiom JDK основан на коде OpenJDK, он поддерживает все существующие сборщики мусора, включая Shenandoah GC, отсутствующий в Oracle Java. Также пользователи Axiom JDK получают доступ к доверенному репозиторию, содержащему проверенных исходные коды Java библиотек, что гарантирует максимальную безопасность приложений. Перейдите по ссылке ниже, чтобы узнать больше о преимуществах Axiom JDK для российской разработки.

Author image

Олег Чирухин

Директор по коммуникациям с разработчиками (DevRel)

Команда Axiom JDK roman.karpov@axiomjdk.ru Команда Axiom JDK logo Axiom Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 Команда Axiom JDK 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67