В этой статье вспомним, что такое Garbage collection (GC), зачем он нужен вообще и какие проблемы решает. Детально рассмотрим режимы работы GC в .NET, поймем, как работает каждый из них, их особенности и различия. Затронем специфику применения некоторых режимов GC в .NET.
Изучим вопрос мониторинга работы GC, какие доступны для этого инструменты и как ими пользоваться.
Введение
Вообще, откуда взялась эта тема? Она появилась из-за поведения наших сервисов, в том числе и на production. Мы увидели, что некоторые приложения начали отнимать 30% CPU. Не могли понять, почему это происходит — ведь по коду все было хорошо. Провели анализ метрик, о которых поговорим позже, и выяснили, что GC потребляет на сборку мусора порядка 30%. И тут возник вопрос — что же с этим делать. Появилось поле для оптимизации. И мы добились хороших результатов, когда после всевозможных манипуляций снизили потребление CPU до 10%, до 5%. Как этого можно добиться, я расскажу ниже.
Когда я задался вопросом и начал готовить эту статью, мне было интересно, а когда у нас появился первый язык, который уже поддерживал сборку мусора. Я даже немного удивился, потому что это был 1964 год. 50 лет назад люди уже задумывались о том, что разработчиков нужно освобождать от занятий с памятью. Это был язык APL. Из языков, которые поддерживают сборку мусора, можно назвать Erlang (1990 год), Eifel, Smalltalk (1972 год), конечно же, C# и любой современный язык, который выходит сейчас, например Go. Это уже must have.
Интересный факт: по исследованиям, разработчики, которые занимаются написанием кода на языках, не поддерживающих сборку мусора, 40% своего продуктивного времени тратят на операции по работе с управлением памятью, что довольно много и, скорее всего, не всегда будет понятно менеджменту.
Что такое Garbage Collection
GC (Garbage Collection — сборка мусора) — высокоуровневая абстракция, которая избавляет разработчиков от необходимости заботиться об освобождении управляемой памяти.
Давайте вспомним основные тезисы по сборке мусора. В .NET сборка мусора основана на трассировке.
Существует понятие корневых элементов приложения. Корневым элементом (root) называется ячейка в памяти, в которой содержится ссылка на размещаемый в куче объект. Строго говоря, корневыми могут называться такие элементы:
Ссылки на глобальные объекты (хотя в C# они не разрешены, но позволяет размещать глобальные объекты).
Ссылки на любые статические объекты или статические поля.
Ссылки на локальные объекты в пределах кодовой базы приложения.
Ссылки на передаваемые методу параметры объекта.
Ссылки на объект, ожидающий финализации.
Любые регистры центрального процессора, которые ссылаются на объект.
Во время процесса сборки мусора исполняющая среда будет исследовать объекты в куче, чтобы определить, являются ли они по-прежнему достижимыми (т. е. корневыми) для приложения. Для этого среда CLR будет создавать графы объектов, представляющие все достижимые для приложения объекты. Кроме того, следует иметь в виду, что сборщик мусора никогда не будет создавать граф для одного и того же объекта дважды, избавляя от необходимости выполнения подсчета циклических ссылок, который характерен для программирования в среде COM.
Фазы сборки мусора:
Маркировка (mark phase).
Чистка (sweep phase).
Сжатие (compact phase).
Поколения объектов: нулевое, первое, второе поколение.
Нулевое и первое поколения еще называют эфемерными поколениями. Они нужны для ускорения отклика нашего приложения.
Для работы приложения CLR инициализирует 2 сегмента виртуального адресного пространства — Small object heap (объекты до 85 КБ) и Large object heap (объекты свыше 85 КБ, в некоторых случаях массивы и связанные списки (linked list), не достигшие данного размера).
Конфигурирование GC довольно простое, что отображено на следующем рисунке:
Рисунок 1. App.config
Конфигурировать режимы работы GC можно путем добавления в app.config секции, показанной на слайде выше, с помощью параметров gcConcurrent, gcServer.
Режим рабочей станции
Рисунок 2. Процесс сборки мусора в режиме рабочей станции
Если мы откроем любую книгу по .NET, любую статью по .NET, где у нас описано, как работает Garbage Collection, обычно это звучит так: работает приложение, не хватает памяти для того, чтобы выделить следующий объект, и происходит запуск GC. При этом все активные потоки приложения приостанавливаются. Это самый простой процесс сборки мусора — workstation non-concurrent mode.
Недостатком этого подхода является то, что во время сборки мусора приложение не занимается ничем другим, кроме сборки мусора. Можно ли этого избежать и как-то повысить отклик нашего приложения?
Идея, как повысить производительность приложения, довольно проста: если нулевое и первое поколения собираются очень быстро, то почему бы их не очищать отдельно от второго поколения. Возможно ли так сделать, чтобы при сборке второго поколения, наше приложение и дальше продолжало аллоцировать объекты? Да, возможно.
Параллельная сборка мусора
Рисунок 3. Параллельная сборка мусора
Для этого существует режим параллельной сборки мусора (workstation concurrent GC).
Параллельная сборка мусора в .NET
До выхода .NET 4.0 очистка неиспользуемых объектов проводилась с применением техники параллельной сборки мусора. В этой модели, при выполнении сбора мусора эфемерных объектов, сборщик мусора временно приостанавливал все активные потоки внутри текущего процесса, чтобы приложение не могло получить доступ к управляемой куче вплоть до завершения процесса сборки мусора.
По завершении цикла сборки мусора приостановленным потокам разрешалось снова продолжить работу. К счастью, в .NET 3.5 сборщик мусора был хорошо оптимизирован, и потому связанные с ним короткие перерывы в работе с приложением редко становились заметными.
Как и оптимизация, параллельная сборка мусора позволяла проводить очистку объектов, которые не были обнаружены ни в одном из эфемерных поколений, в отдельном потоке. Это сокращало (но не устраняло) необходимость в приостановке активных потоков исполняющей средой .NET. Тем более, параллельная сборка мусора позволяла размещать объекты в куче во время сборки объектов неэфемерных поколений.
Фоновая сборка мусора
Рисунок 4. Фоновая сборка мусора
В .NET 4.0 сборщик мусора по-другому решает вопрос о приостановке потоков и очистке объектов в управляемой куче, используя при этом технику фоновой сборки мусора. Несмотря на ее название, это вовсе не означает, что вся сборка мусора теперь происходит в дополнительных фоновых потоках выполнения. На самом деле, в случае фоновой сборки мусора для объектов, не относящихся к эфемерному поколению, исполняющая среда .NET теперь может проводить сборку мусора объектов эфемерного поколения в отдельном фоновом потоке.
Механизм сборки мусора в .NET 4.0 был улучшен так, чтобы на приостановку потока, связанного с деталями сбора мусора, требовалось меньше времени. Благодаря этим изменениям процесс очистки неиспользуемых объектов поколения 0 и 1 стал оптимальным. Он позволяет получать более высокий уровень производительности приложений.
Давайте представим, что у нас на хосте, где наше приложение запустится, есть один процессор. В таком случае, что бы мы ни делали, мы все равно запустимся в режиме рабочей станции. Вы можете делать с флажками что угодно, но на одном процессоре не хватит одновременных потоков, которые могут запуститься для того, чтобы обслуживать другой режим.
Режим сервера
Особенности работы GC в режиме сервера.
Сборка выполняется в нескольких выделенных потоках, выполняемых с приоритетом THREAD_PRIORITY_HIGHEST .
Для каждого процессора предоставляется куча и выделенный поток, выполняющий сборку мусора, и сборка куч выполняется одновременно. Каждая куча содержит кучу небольших объектов и кучу больших объектов, и все кучи доступны из пользовательского кода. Объекты из различных куч могут ссылаться друг на друга.
Так как несколько потоков сборки мусора работают совместно, для кучи одного и того же размера сборка мусора сервера выполняется быстрее сборки мусора рабочей станции.
В сборке мусора сервера часто используются сегменты большего размера. Однако обратите внимание, что это только обобщение: размер сегмента зависит от реализации и может изменяться. При настройке приложения не следует делать никаких предположений относительно размера сегментов, выделенных сборщиком мусора.
Сборка мусора сервера может оказаться ресурсоемкой операцией. Например, если на компьютере с 4 процессорами выполняется 12 процессов, в каждом из которых применяется сборка мусора сервера, будут использоваться 48 выделенных потоков сборки мусора. В случае высокой загрузки памяти, если все процессы запускают сборку мусора, сборщику мусора понадобится выполнить планирование работы 48 потоков.
При запуске сотен экземпляров приложения рассмотрите возможность использования сборки мусора рабочей станции с отключенной параллельной сборкой мусора. Это уменьшит число переключений контекста, что может повысить быстродействие.
Рисунок 6. Визуализация работы Garbage Collection в режиме сервера
На рисунке 6 показана визуализация того, как все это работает в режиме сервера. Как видим, главное отличие заключается в том, что сборка мусора выполняется для каждого доступного процессора.
Рисунок 7. Server Background Mode
Начиная с .NET Framework 4.5, фоновая сборка мусора сервера является режимом по умолчанию для сборки мусора сервера. Этот режим функционирует аналогично фоновой сборке мусора рабочей станции, описанной выше, однако с некоторыми отличиями. Для фоновой сборки мусора рабочей станции используется один выделенный поток фоновой сборки мусора, тогда как для фоновой сборки мусора сервера используется несколько потоков — обычно по одному выделенному потоку для каждого логического процессора.
Инструменты мониторинга
GC class
Что можно сделать с помощь GC class из кода подробно описано в статье, но стоит сразу отметить, что это будет просто логирование нужной нам информации в лог, а затем анализ этой информации с помощью каких-то доступных средств. Не очень хороший способ — это не выход из ситуации.
Performance Monitor
Одним из самых мощных инструментов для обнаружения проблем с производительностью в Windows являются встроенные счетчики производительности, так называемые Performance counters. Оснастка Performance monitor — основной инструмент для управления ими.
Performance Viewer
Performance Viewer основан на трассировке событий Windows. Чуть позже поговорим о том, что это такое, зачем это нужно и что можно вообще мониторить с его помощью.
SOS Debugging Extension
SOS Debugging Extension стоит отметить, но уже мало кто использует этот инструмент.
dotMemory
Платный представитель от JetBrains. Стоит отметить, что его open source конкуренты на текущий момент мало в чем ему уступают.
Concurrency Vizualizer
Concurrency Vizualizer — расширение для Visual Studio. К мониторингу памяти относится очень косвенно. При этом оно очень информативное, так как позволяет увидеть множество параметров по работе приложения в многопоточной среде. С помощью этой утилиты можно проанализировать, когда потоки приостанавливаются, восстанавливают свою работу и т. д.
Performance Monitor
Рисунок 8. Счетчики Performance Monitor
Какие счетчики (counter) предлагает Performance Monitor? Первый счетчик, на который стоит обратить внимание — это процент времени, которое было потрачено самим GC. Этот счетчик делает замеры между двумя сборками мусора, считает циклы процессора, циклы, которые были потрачены в общем и которые были потрачены на сборку мусора. Например, если между двумя сборками прошел 1 миллион циклов процессора и при этом из них 300 тысяч потрачено на сборку мусора, то, соответственно, наше приложение 30% времени тратит просто для того, чтобы собирать мусор.
На какое значение нужно обращать внимание? Это довольно сложный вопрос. К примеру, мы получили цифру 17. Что мне с этой цифрой делать дальше? Из опыта рекомендую обращать внимание на значение 50%. Если 50% — значит половину времени мы тратим впустую. Если это время тратится еще в дата-центрах, то тратятся деньги. И с этим надо что-то делать. Если мы видим цифру в 10 %, то для того, чтобы опустить ее на 5, нужно потратить столько денег, что даже не стоит в это вкладываться.
Следующий параметр, на который стоит обращать внимание — Allocated bytes/second. Он показывает число байтов в секунду, которые мы можем аллоцировать в памяти. Можем посмотреть, какой размер занимает нулевое поколение, первое, второе поколение, сколько занимает Large Object Heap, как перетекают объекты из нулевого поколения в первое, из первого — во второе, количество выживших объектов и т. д.
Finalization Survivors — это счетчик, который показывает количество объектов, которые ушли с очереди финализации и готовы к тому, чтобы началась их чистка.
Пример, как использовать этот инструмент, показан на рисунке 9.
Рисунок 9. Работа с Performance Monitor
Performance Viewer
На мой взгляд, это один из лучших инструментов на текущий момент. Также радует, что производители уже начали задумываться о том, что же делать с Linux, что очень актуально для приложений, написанных под .NET Core. Уже сейчас на их сайте есть небольшой туториал, как снимать метрики с докер хостов. Надеюсь, они будут продолжать развиваться, и мы получим очень хороший инструмент.
Инструмент позволяет мониторить практически все аспекты, которые нужны разработчику для анализа: CPU, стек, есть возможность сделать дамп памяти и проанализировать его, можно посмотреть статистику по GC.
Рисунок 10. Работа с Performance Viewer
Инструмент довольно простой (см. рисунок 10): нажимаем collect и собираем нужные нам метрики. Совет для тех, кто будет использовать — не собирайте метрики долго. Сделал большую ошибку: собрал метрики за минуту и потом ждал пока распарсится минут семь, потом бросил. Должно хватить чтобы понять, что происходит с вашим приложением и что с ним делать. Инструмент может показать все, что связано с GC. На слайде выделены данные, которые касаются GC-статистики. Показывается режим GC, в котором запущено наше приложение, время, которое было потрачено на паузы GC, процессорное время, которое было потрачено, количество сборок мусора в каждом из поколений, минимальные паузы, пики.
События трассировки
Если посмотреть определение в MSDN или в литературе, то трассировка событий — это высокоэффективная масштабируемая система трассировки с минимальными затратами ресурсов, которая реализуется в Windows. Если немного заглянуть под капот, то очень грубо говоря, этот процесс выглядит так: мы запускаем трассировку наших приложений, это все ложится в обычные файлики, эти файлики потом парсятся, и мы исследуем, что происходит с нашим приложением.
Что вообще можно мониторить в .NET в среде CLR? GC, Runtime, Exceptions, Thread pool, Stack и т. д. Детально о всех метриках можно почитать здесь.
Cейчас мы рассмотрим Garbage Collection в событиях (event), и что они нам позволяют мониторить. Они нам позволяют собирать сведения, которые как раз и относятся к сборке мусора: когда она началась, когда закончилась, в каком поколении. Как долго длилась не покажут — нужно вычислять самому, и это нетривиальная задача. Нетривиальная потому, что если мы посмотрим на режим рабочей станции, когда у нас нет никаких конкурентных режимов, то там все просто: потоки остановились, приостановились, возобновились. И эту дельту мы можем словить по разнице. Когда мы вспоминаем высокоприоритетную сборку мусора, то тут уже все далеко не тривиально. Поэтому уже лучше пользоваться теми инструментами, которые у нас есть.
На GitHub есть библиотеки, которые позволяют научиться работать с данными событиями. К примеру, TraceEvent Library позволяет нам написать приложение, которое будет выполнять трассировку другого приложения. И всю эту информацию спокойно собирать, дебажить и что-то с ней делать.
На рисунке 11 показан небольшой пример, как можно запустить трассировку событий используя TraceEvent Library.
Рисунок 11. Пример кода
На рисунке 12 происходит магия в части того, как мы собираем все эти счетчики. А вот что из всего этого вышло уже отображено на рисунке 13.
Рисунок 12. Пример кода
Мы получили следующую информацию: когда у нас начал выполняться GC и сколько времени заняла пауза на GC, какая по счету сборка мусора, с каким поколением работал GC, в каком режиме работает наше приложение.
GC-визуализация
Рисунок 13. Визуализация GC
Есть довольно интересный блог, который ведет Мэт Уоррен. В нем можно найти очень много интересной и полезной информации: как работает Garbage Collection, что же происходит на самом деле «под капотом».
Рисунок 14. Визуализация GC от Мэта Уоррена
На рисунке 14 отображена визуализация работы GC, основанная на трассировке событий, написанная автором блога. Всем, кому интересно понять, как же работает GC, рекомендую разобраться с ним.
Рисунок 15. Таблица данных тестирования в разных режимах
В следующей таблице собраны метрики, полученные в результате тестирования одного и того же приложения в разных режимах. Было запущено приложение, основной задачей которого была генерация memory-трафика. Что мы видим? Серверный режим, действительно, уменьшает паузы работы GC, уменьшает количество запусков итераций сборки мусора, но это все делается за счет более интенсивного использования CPU и за счет более интенсивного потребления памяти. Об этом всегда нужно помнить. Если у нас десктопное приложение, в котором нам нужен максимальный отклик, то этот режим явно не для него.
Выводы
Каждый из нас рано или поздно сталкивается с проблемами неоптимальной работы написанного приложения, причины могут быть разные. При их анализе довольно часто мы не смотрим на то, как в таких случаях работает GC, как его работа влияет на работу приложения, оптимальный ли режим GC выбран именно для текущего приложения. А ведь многие ответы как раз и могут быть получены при таком анализе.
Полезные ссылки
Garbage Collection
Maoni’s WebLog
The .NET Framework 4.5 includes new garbage collector enhancements for client and server apps
Visualising the .NET Garbage Collector
Examples of GC visualization
PerfView
CLR ETW Events
Memory Performance counters
Все про українське ІТ в телеграмі — підписуйтеся на канал DOU
Что такое сборка мусора в Java?
Сборка мусора — это процесс восстановления заполненной памяти среды выполнения путем уничтожения неиспользуемых объектов.
В таких языках, как C и C++, программист отвечает как за создание, так и за уничтожение объектов. Иногда программист может забыть уничтожить бесполезные объекты, и выделенная им память не освобождается. Расходуется все больше и больше системной памяти, и в конечном итоге она больше не выделяется. Такие приложения страдают от “утечек памяти”.
После определенного момента памяти уже не хватает для создания новых объектов, и программа нештатно завершается из-за OutOfMemoryErrors .
В C++ для сборки мусора можно воспользоваться методом delete() , а в C — методом free() . В Java сборка мусора происходит автоматически в течение всего времени работы программы. Это устраняет необходимость выделения памяти и, следовательно, позволяет избежать утечек.
Сборка мусора в Java — это процесс, с помощью которого программы Java автоматически управляют памятью. Java-программы компилируются в байт-код, который запускается на виртуальной машине Java (JVM).
Когда Java-программы выполняются на JVM, объекты создаются в куче, которая представляет собой часть памяти, выделенную для них.
Пока Java-приложение работает, в нем создаются и запускаются новые объекты. В конце концов некоторые объекты перестают быть нужны. Можно сказать, что в любой момент времени память кучи состоит из двух типов объектов.
Живые — эти объекты используются, на них ссылаются откуда-то еще.
Мертвые — эти объекты больше нигде не используются, ссылок на них нет.
Сборщик мусора находит эти неиспользуемые объекты и удаляет их, чтобы освободить память.
Как разыменовать объект в Java
Основная цель сборки мусора — освободить память кучи, уничтожив объекты, которые не содержат ссылку. Когда на объект нет ссылки, предполагается, что он мертв и больше не нужен. Таким образом, память, занятая объектом, может быть восстановлена.
Есть несколько способов убрать ссылки на объект и сделать его кандидатом на сборку мусора. Вот некоторые из них.
Сделать ссылку нулевой
Student student = new Student(); student = null;
Назначить ссылку другому объекту
Student studentOne = new Student(); Student studentTwo = new Student(); studentOne = studentTwo; // Т еперь первый объект, на который ссылается studentOne, доступен для сбора мусора
Использовать анонимный объект
register(new Student());
Как работает сборка мусора в Java?
Сборка мусора в Java — автоматический процесс. Программисту не нужно явно отмечать объекты, подлежащие удалению.
Сборка мусора производится в JVM. Каждая JVM может реализовать собственную версию сборки мусора. Однако сборщик должен соответствовать стандартной спецификации JVM для работы с объектами, присутствующими в памяти кучи, для маркировки или идентификации недостижимых объектов и их уничтожения через уплотнение.
Каковы источники для сборки мусора в Java?
Сборщики мусора работают с концепцией корней сбора мусора (GC Roots) для идентификации живых и мертвых объектов.
Примеры таких корней.
Классы, загружаемые системным загрузчиком классов (не пользовательские загрузчики классов).
Живые потоки.
Локальные переменные и параметры выполняемых в данный момент методов.
Локальные переменные и параметры методов JNI.
Глобальная ссылка на JNI.
Объекты, применяемые в качестве монитора для синхронизации.
Объекты, удерживаемые из сборки мусора JVM для своих целей.
Сборщик мусора просматривает весь граф объектов в памяти, начиная с этих корней и следуя ссылкам на другие объекты.
Этапы сборки мусора в Java
Стандартная реализация сборки мусора включает в себя три этапа.
Пометка объектов как живых
На этом этапе GC (сборщик мусора) идентифицирует все живые объекты в памяти путем обхода графа объектов.
Когда GC посещает объект, то помечает его как доступный и, следовательно, живой. Все объекты, недоступные из корней GC, рассматриваются как кандидаты на сбор мусора.
Зачистка мертвых объектов
После фазы разметки пространство памяти занято либо живыми (посещенными), либо мертвыми (не посещенными) объектами. Фаза зачистки освобождает фрагменты памяти, которые содержат эти мертвые объекты.
Компактное расположение оставшихся объектов в памяти
Мертвые объекты, которые были удалены во время предыдущей фазы, не обязательно находились рядом друг с другом. Поэтому вы рискуете получить фрагментированное пространство памяти.
Память можно уплотнить, когда сборщик мусора удалит мертвые объекты. Оставшиеся будут располагаться в непрерывном блоке в начале кучи.
Процесс уплотнения облегчает последовательное выделение памяти для новых объектов.
Что такое сбор мусора по поколениям?
Сборщики мусора в Java реализуют стратегию сбора мусора поколений, которая классифицирует объекты по возрасту.
Необходимость отмечать и уплотнять все объекты в JVM неэффективна. По мере выделения все большего количества объектов их список растет, что приводит к увеличению времени сбора мусора. Эмпирический анализ приложений показал, что большинство объектов в Java недолговечны.
В приведенном выше примере ось Y показывает количество выделенных байтов, а ось X — количество выделенных байтов с течением времени. Как видно, со временем все меньше и меньше объектов сохраняют выделенную память.
Большинство объектов живут очень мало, что соответствует более высоким значениям в левой части графика. Вот почему Java классифицирует объекты по поколениям и выполняет сборку мусора в соответствии с ними.
Область памяти кучи в JVM разделена на три секции:
Молодое поколение
Вновь созданные объекты начинаются в молодом поколении. Молодое поколение далее подразделяется на две категории.
Пространство Эдема — все новые объекты начинают здесь, и им выделяется начальная память.
Пространства выживших (FromSpace и ToSpace) — объекты перемещаются сюда из Эдема после того, как пережили один цикл сборки мусора.
Процесс, когда объекты собираются в мусор из молодого поколения, называется малымсобытием сборки мусора.
Когда пространство Эдема заполнено объектами, выполняется малая сборка мусора. Все мертвые объекты удаляются, а все живые — перемещаются в одно из оставшихся двух пространств. Малая GC также проверяет объекты в пространстве выживших и перемещает их в другое (следующее) пространство выживших.
Возьмем в качестве примера следующую последовательность.
В Эдеме есть объекты обоих типов (живые и мертвые).
Происходит малая GC — все мертвые объекты удаляются из Эдема. Все живые объекты перемещаются в пространство-1 (FromSpace). Эдем и пространство-2 теперь пусты.
Новые объекты создаются и добавляются в Эдем. Некоторые объекты в Эдеме и пространстве-1 становятся мертвыми.
Происходит малая GC — все мертвые объекты удаляются из Эдема и пространства-1. Все живые объекты перемещаются в пространство-2 (ToSpace). Эдем и пространство-2 снова пусты.
Таким образом, в любое время одно из пространств для выживших всегда пусто. Когда выжившие объекты достигают определенного порога перемещения по пространствам выживших, они переходят в старшее поколение.
Для установки размера молодого поколения можно воспользоваться флагом -Xmn .
Старшее поколение
Объекты-долгожители в конечном итоге переходят из молодого поколения в старшее. Оно также известно как штатное поколение и содержит объекты, которые долгое время оставались в пространствах выживших.
Пороговое значение срока службы объекта определяет, сколько циклов сборки мусора он может пережить, прежде чем будет перемещен в старшее поколение.
Процесс, когда объекты отправляются в мусор из старшего поколения, называется основным событием сборки мусора.
Для установки начального и максимального размера памяти кучи вы можете воспользоваться флагами -Xms и -Xmx .
Поскольку Java задействует сборку мусора по поколениям, то чем больше событий сборки мусора переживает объект, тем дальше он продвигается в куче. Он начинает в молодом поколении и в конечном итоге заканчивает в штатном поколении, если проживет достаточно долго.
Чтобы понять продвижение объектов между пространствами и поколениями, рассмотрим следующий пример.
Когда объект создается, он сначала помещается в пространство Эдема молодого поколения. Как только происходит малая сборка мусора, живые объекты из Эдема перемещаются в пространство FromSpace. Когда происходит следующая малая сборка мусора, живые объекты как из Эдема, так и из пространства перемещаются в пространство ToSpace.
Этот цикл продолжается определенное количество раз. Если объект все еще “в строю” после этого момента, следующий цикл сборки мусора переместит его в пространство старшего поколения.
Постоянное поколение
Метаданные, такие как классы и методы, хранятся в постоянном поколении. JVM заполняет его во время выполнения на основе классов, используемых приложением. Классы, которые больше не используются, могут переходить из постоянного поколения в мусор.
Для установки начального и максимального размера постоянного поколения вы можете воспользоваться флагами -XX:PermGen и -XX:MaxPermGen .
Мета-пространство
Начиная с Java 8, на смену пространству постоянного поколения (PermGen) приходит пространство памяти MetaSpace. Реализация отличается от PermGen — это пространство кучи теперь изменяется автоматически.
Это позволяет избежать проблемы нехватки памяти у приложений, которая возникает из-за ограниченного размера пространства PermGen в куче. Память мета-пространства может быть собрана как мусор, и классы, которые больше не используются, будут автоматически очищены, когда мета-пространство достигнет максимального размера.
Типы сборщиков мусора в виртуальной машине Java
Сборка мусора повышает эффективности памяти в Java, поскольку объекты без ссылок удаляются из памяти кучи и освобождается место для новых объектов.
У виртуальной машины Java есть восемь типов сборщиков мусора. Рассмотрим каждый из них в деталях.
Серийный GC
Это самая простая реализация GC. Она предназначена для небольших приложений, работающих в однопоточных средах. Все события сборки мусора выполняются последовательно в одном потоке. Уплотнение выполняется после каждой сборки мусора.
Запуск сборщика приводит к событию “остановки мира”, когда все приложение приостанавливает работу. Поскольку на время сборки мусора все приложение замораживается, не следует прибегать к такому в реальной жизни, если требуется, чтобы задержки были минимальными.
Аргумент JVM для использования последовательного сборщика мусора -XX:+UseSerialGC .
Параллельный GC
Параллельный сборщик мусора предназначен для приложений со средними и большими наборами данных, которые выполняются на многопроцессорном или многопоточном оборудовании. Это реализация GC по умолчанию, и она также известна как сборщик пропускной способности.
Несколько потоков предназначаются для малой сборки мусора в молодом поколении. Единственный поток занят основной сборкой мусора в старшем поколении.
Запуск параллельного GC также вызывает “остановку мира”, и приложение зависает. Такое больше подходит для многопоточной среды, когда требуется завершить много задач и допустимы длительные паузы, например при выполнении пакетного задания.
Аргумент JVM для использования параллельного сборщика мусора: -XX:+UseParallelGC .
Старый параллельный GC
Это версия Parallel GC по умолчанию, начиная с Java 7u4. Это то же самое, что и параллельный GC, за исключением того, что в нем применяются несколько потоков как для молодого поколения, так и для старшего поколения.
Аргумент JVM для использования старого параллельного сборщика мусора: -XX:+UseParallelOldGC .
CMS (Параллельная пометка и зачистка) GC
Также известен как параллельный сборщик низких пауз. Для малой сборки мусора задействуются несколько потоков, и происходит это через такой же алгоритм, как в параллельном сборщике. Основная сборка мусора многопоточна, как и в старом параллельном GC, но CMS работает одновременно с процессами приложений, чтобы свести к минимуму события “остановки мира”.
Из-за этого сборщик CMS потребляет больше ресурсов процессора, чем другие сборщики. Если у вас есть возможность выделить больше ЦП для повышения производительности, то CMS предпочтительнее, чем простой параллельный сборщик. В CMS GC не выполняется уплотнение.
Аргумент JVM для использования параллельного сборщика мусора с разверткой меток: -XX:+UseConcMarkSweepGC .
G1 (Мусор — первым) GC
G1GC был задуман как замена CMS и разрабатывался для многопоточных приложений, которые характеризуются крупным размером кучи (более 4 ГБ). Он параллелен и конкурентен, как CMS, но “под капотом” работает совершенно иначе, чем старые сборщики мусора.
Хотя G1 также действует по принципу поколений, в нем нет отдельных пространств для молодого и старшего поколений. Вместо этого каждое поколение представляет собой набор областей, что позволяет гибко изменять размер молодого поколения.
G1 разбивает кучу на набор областей одинакового размера (от 1 МБ до 32 МБ — в зависимости от размера кучи) и сканирует их в несколько потоков. Область во время выполнения программы может неоднократно становиться как старой, так и молодой.
После завершения этапа разметки G1 знает, в каких областях содержится больше всего мусора. Если пользователь заинтересован в минимизации пауз, G1 может выбрать только несколько областей. Если время паузы неважно для пользователя или предел этого времени установлен высокий, G1 пройдет по большему числу областей.
Поскольку G1 GC идентифицирует регионы с наибольшим количеством мусора и сначала выполняет сбор мусора в них, он и называется: “Мусор — первым”.
Помимо областей Эдема, Выживших и Старой памяти, в G1GC присутствуют еще два типа.
Humongous (Огромная) — для объектов большого размера (более 50% размера кучи).
Available (Доступная) — неиспользуемое или не выделенное пространство.
Аргумент JVM для использования сборщика мусора G1: -XX:+UseG1GC .
Сборщик мусора Эпсилон
Epsilon — сборщик мусора, который был выпущен как часть JDK 11. Он обрабатывает выделение памяти, но не реализует никакого реального механизма восстановления памяти. Как только доступная куча исчерпана, JVM завершает работу.
Его можно задействовать для приложений, чувствительных к сверхвысокой задержке, где разработчики точно знают объем памяти приложения или даже добиваются ситуации (почти) полной свободы от мусора. В противном случае пользоваться Epsilon GC не рекомендуется.
Аргумент JVM для использования сборщика мусора Epsilon: -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC .
Шенандоа
Shenandoah — новый GC, выпущенный как часть JDK 12. Ключевое преимущество Shenandoah перед G1 состоит в том, что большая часть цикла сборки мусора выполняется одновременно с потоками приложений. G1 может эвакуировать области кучи только тогда, когда приложение приостановлено, а Shenandoah перемещает объекты одновременно с приложением.
Shenandoah может компактировать живые объекты, очищать мусор и освобождать оперативную память почти сразу после обнаружения свободной памяти. Поскольку все это происходит одновременно, без приостановки работы приложения, то Shenandoah более интенсивно нагружает процессор.
Аргумент JVM для сборщика мусора Шенандоа: -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC .
ZGC
ZGC — еще один GC, выпущенный как часть JDK 11 и улучшенный в JDK 12. Он предназначен для приложений, которые требуют низкой задержки (паузы в менее чем 10 мс) и/или задействуют очень большую кучу (несколько терабайт).
Основные цели ZGC — низкая задержка, масштабируемость и простота в применении. Для этого ZGC позволяет Java-приложению продолжать работу, пока выполняются все операции по сбору мусора. По умолчанию ZGC освобождает неиспользуемую память и возвращает ее в операционную систему.
Таким образом, ZGC привносит значительное улучшение по сравнению с другими традиционными GCS, обеспечивая чрезвычайно низкое время паузы (обычно в пределах 2 мс).
Аргумент JVM для использования сборщика мусора ZGC: — XX:+UnlockExperimentalVMOptions -XX:+UseZGC .
Примечание: как Shenandoah, так и ZGC планируется вывести из экспериментальной стадии в продакшен при выпуске JDK 15.
Как правильно выбрать сборщик мусора
Если у вашего приложения нет строгих требований ко времени задержки, вам стоит просто запустить приложение и предоставить выбор правильного сборщика самой JVM.
В большинстве случаев настройки по умолчанию отлично работают. При необходимости можно настроить размер кучи для повышения производительности. Если производительность по-прежнему не соответствует ожиданиям, попробуйте изменить сборщик в соответствии с требованиями вашего приложения.
Последовательный. Если в приложении небольшой набор данных (примерно до 100 МБ), и/или оно будет работать на одном процессоре без каких-либо требований к времени задержки.
Параллельный. Если приоритет — пиковая производительность приложения, и требования к времени задержки отсутствуют (или допустимы паузы в одну секунду и более).
CMS/G1. Если время отклика важнее, чем общая пропускная способность, и паузы при сборке мусора должны быть короче одной секунды.
ZGC. Если у времени отклика высокий приоритет и/или задействована очень большая куча.
Преимущества сборки мусора
У сборки мусора в Java множество преимуществ.
Прежде всего, это упрощает код. Не нужно беспокоиться о правильном назначении памяти и циклах высвобождения. Вы просто прекращаете использовать объект в коде, и память, которую он занимал, в какой-то момент автоматически восстановится.
Программистам, работающим на языках без сборки мусора (таких как C и C++), приходится реализовывать ручное управление памятью у себя в коде.
Также повышается эффективность памяти Java, поскольку сборщик мусора удаляет из памяти кучи объекты без ссылок. Это освобождает память кучи для размещения новых объектов.
Некоторые программисты выступают за ручное управление памятью вместо сборки мусора, но сборка мусора — уже стандартный компонент многих популярных языков программирования.
Для сценариев, когда сборщик мусора негативно влияет на производительность, Java предлагает множество вариантов настройки, повышающих эффективность GC.
Рекомендации по сбору мусора
Избегайте ручных триггеров
Помимо основных механизмов сборки мусора, один из важнейших моментов относительно этого процесса в Java — недетерминированность. То есть невозможно предсказать, когда именно во время выполнения она произойдет.
С помощью методов System.gc() или Runtime.gc() можно включить в код подсказку для запуска сборщика мусора, но это не гарантирует, что он действительно запустится.
Пользуйтесь инструментами для анализа
Если у вас недостаточно памяти для запуска приложения, вы столкнетесь с замедлениями, длительным временем сбора мусора, событиями “остановки мира” и, в конечном итоге, ошибками из-за нехватки памяти. Возможно, это указывает, что куча слишком мала, но также может и значить, что в приложении произошла утечка памяти.
Вы можете прибегнуть к помощи инструмента мониторинга, например jstat или Java Flight Recorder, и увидеть, растет ли использование кучи бесконечно, что может указывать на ошибку в коде.
Отдавайте предпочтение настройкам по умолчанию
Если у вас небольшое автономное Java-приложение, вам, скорее всего, не понадобится настраивать сборку мусора. Настройки по умолчанию отлично вам послужат.
Пользуйтесь флагами JVM для настройки
Лучший подход к настройке сборки мусора в Java — установка JVM-флагов. С помощью флагов можно задать сборщик мусора (например, Serial, G1 и т.д.), начальный и максимальный размер кучи, размер разделов кучи (например, Молодого поколения, Старшего поколения) и многое другое.
Выбирайте сборщик правильно
Хороший ориентир в плане начальных настроек — характер настраиваемого приложения. К примеру, параллельный сборщик мусора эффективен, но часто вызывает события “остановки мира”, что делает его более подходящим для внутренней обработки, где допустимы длительные паузы.
С другой стороны, сборщик мусора CMS предназначен для минимизации задержек, а значит идеально подходит для веб-приложений, где важна скорость реагирования.
Вывод
В этой статье мы обсудили сборку мусора Java, механизм ее работы и ее типы.
Для многих простых Java-приложений программисту нет необходимости сознательно отслеживать сборку мусора. Однако тем, кто хочет развить навыки работы с Java, важно понимать, как происходит данный процесс.
Это также очень популярный вопрос на собеседованиях: как на миддл-, так и на сеньор-позиции в бэкенд-разработке.
Спасибо за чтение.
9 советов, как выделиться среди Java-разработчиков
Состояния потоков в Java
Новый подход к пониманию RxJava
Сборка мусора
Управление памятью в JavaScript выполняется автоматически и незаметно. Мы создаём примитивы, объекты, функции… Всё это занимает память.
Но что происходит, когда что-то больше не нужно? Как движок JavaScript обнаруживает, что пора очищать память?
Достижимость
Основной концепцией управления памятью в JavaScript является принцип достижимости.
Если упростить, то «достижимые» значения – это те, которые доступны или используются. Они гарантированно находятся в памяти.
Существует базовое множество достижимых значений, которые не могут быть удалены. Например:
Выполняемая в данный момент функция, её локальные переменные и параметры.
Другие функции в текущей цепочке вложенных вызовов, их локальные переменные и параметры.
Глобальные переменные.
(некоторые другие внутренние значения)
Эти значения мы будем называть корнями.
В движке JavaScript есть фоновый процесс, который называется сборщиком мусора. Он отслеживает все объекты и удаляет те, которые стали недоступными.
Простой пример
Вот самый простой пример:
// в user находится ссылка на объект let user = < name: "John" >;
Здесь стрелка обозначает ссылку на объект. Глобальная переменная user ссылается на объект (мы будем называть его просто «John» для краткости). В свойстве «name» объекта John хранится примитив, поэтому оно нарисовано внутри объекта.
Если перезаписать значение user , то ссылка потеряется:
user = null;
Теперь объект John становится недостижимым. К нему нет доступа, на него нет ссылок. Сборщик мусора удалит эти данные и освободит память.
Две ссылки
Представим, что мы скопировали ссылку из user в admin :
// в user находится ссылка на объект let user = < name: "John" >; let admin = user;
Теперь, если мы сделаем то же самое:
user = null;
…то объект John всё ещё достижим через глобальную переменную admin , поэтому он находится в памяти. Если бы мы также перезаписали admin , то John был бы удалён.
Взаимосвязанные объекты
Теперь более сложный пример. Семья:
function marry(man, woman) < woman.husband = man; man.wife = woman; return < father: man, mother: woman >> let family = marry(< name: "John" >, < name: "Ann" >);
Функция marry «женит» два объекта, давая им ссылки друг на друга, и возвращает новый объект, содержащий ссылки на два предыдущих.
Недостаточно удалить только одну из этих двух ссылок, потому что все объекты останутся достижимыми.
Но если мы удалим обе, то увидим, что у объекта John больше нет входящих ссылок:
Исходящие ссылки не имеют значения. Только входящие ссылки могут сделать объект достижимым. Объект John теперь недостижим и будет удалён из памяти со всеми своими данными, которые также стали недоступны.
После сборки мусора:
Недостижимый «остров»
Вполне возможна ситуация, при которой целый «остров» взаимосвязанных объектов может стать недостижимым и удалиться из памяти.
Возьмём объект family из примера выше. А затем:
family = null;
Структура в памяти теперь станет такой:
Этот пример демонстрирует, насколько важна концепция достижимости.
Объекты John и Ann всё ещё связаны, оба имеют входящие ссылки, но этого недостаточно.
Бывший объект family был отсоединён от корня, на него больше нет ссылки, поэтому весь «остров» становится недостижимым и будет удалён.
Внутренние алгоритмы
Основной алгоритм сборки мусора называется «алгоритм пометок» (от англ. «mark-and-sweep»).
Согласно этому алгоритму, сборщик мусора регулярно выполняет следующие шаги:
Сборщик мусора «помечает» (запоминает) все корневые объекты.
Затем он идёт по ним и «помечает» все ссылки из них.
Затем он идёт по отмеченным объектам и отмечает их ссылки. Все посещённые объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды.
…И так далее, пока не будут посещены все достижимые (из корней) ссылки.
Все непомеченные объекты удаляются.
Например, пусть наша структура объектов выглядит так:
Мы ясно видим «недостижимый остров» справа. Теперь давайте посмотрим, как будет работать «алгоритм пометок» сборщика мусора.
На первом шаге помечаются корни:
Затем помечаются объекты по их ссылкам:
…А затем объекты по их ссылкам и так далее, пока это возможно:
Теперь объекты, которые не удалось посетить в процессе, считаются недостижимыми и будут удалены:
Мы также можем представить себе этот процесс как выливание огромного ведра краски из корней, которая течёт по всем ссылкам и отмечает все достижимые объекты. Затем непомеченные удаляются.
Это концепция того, как работает сборка мусора. Движки JavaScript применяют множество оптимизаций, чтобы она работала быстрее и не задерживала выполнение кода.
Вот некоторые из оптимизаций:
Сборка по поколениям (Generational collection) – объекты делятся на два набора: «новые» и «старые». В типичном коде многие объекты имеют короткую жизнь: они появляются, выполняют свою работу и быстро умирают, так что имеет смысл отслеживать новые объекты и, если это так, быстро очищать от них память. Те, которые выживают достаточно долго, становятся «старыми» и проверяются реже.
Инкрементальная сборка (Incremental collection) – если объектов много, и мы пытаемся обойти и пометить весь набор объектов сразу, это может занять некоторое время и привести к видимым задержкам в выполнении скрипта. Так что движок делит всё множество объектов на части, и далее очищает их одну за другой. Получается несколько небольших сборок мусора вместо одной всеобщей. Это требует дополнительного учёта для отслеживания изменений между частями, но зато получается много крошечных задержек вместо одной большой.
Сборка в свободное время (Idle-time collection) – чтобы уменьшить возможное влияние на производительность, сборщик мусора старается работать только во время простоя процессора.
Существуют и другие способы оптимизации и разновидности алгоритмов сборки мусора. Но как бы мне ни хотелось описать их здесь, я должен воздержаться, потому что разные движки реализуют разные хитрости и методы. И, что ещё более важно, все меняется по мере развития движков, поэтому изучать тему глубоко «заранее», без реальной необходимости, вероятно, не стоит. Если, конечно, это не вопрос чистого интереса, тогда для вас будет несколько ссылок ниже.
Итого
Главное, что нужно знать:
Сборка мусора выполняется автоматически. Мы не можем ускорить или предотвратить её.
Объекты сохраняются в памяти, пока они достижимы.
Если на объект есть ссылка – вовсе не факт, что он является достижимым (из корня): набор взаимосвязанных объектов может стать недоступен в целом, как мы видели в примере выше.
Современные движки реализуют разные продвинутые алгоритмы сборки мусора.
О многих из них рассказано в прекрасной книге о сборке мусора «The Garbage Collection Handbook: The Art of Automatic Memory Management» (R. Jones и др.).
Если вы знакомы с низкоуровневым программированием, то более подробная информация о сборщике мусора V8 находится в статье A tour of V8: Garbage Collection.
Также в блоге V8 время от времени публикуются статьи об изменениях в управлении памятью. Разумеется, чтобы изучить сборку мусора, вам лучше подготовиться, узнав о том как устроен движок V8 внутри в целом и почитав блог Вячеслава Егорова, одного из инженеров, разрабатывавших V8. Я говорю про «V8», потому что он лучше всего освещается в статьях в Интернете. Для других движков многие подходы схожи, но сборка мусора отличается во многих аспектах.
Глубокое понимание работы движков полезно, когда вам нужна низкоуровневая оптимизация. Было бы разумно запланировать их изучение как следующий шаг после того, как вы познакомитесь с языком.
Garbage Collection наглядно
В последнее время я работаю с клиентами над вопросами настроек JVM. Смахивает ситуация на то, что далеко не все из разработчиков и администраторов знают о том, как работает garbage collection и о том, как JVM использует память. Поэтому я решил дать вводную в эту тему с наглядным примером. Пост не претендует на то, чтобы покрыть весь объем знаний о garbage collection или настройке JVM (он огромен), ну и, в конце концов, об этом много чего хорошего написано уже в Сети.
Пост посвящён HotSpot JVM – ‘обычной’ JVM от Oracle (раньше Sun), JVM, которую вы скорее всего будете использовать в Windows. В случае Linux это может быть open source JVM. Или JVM может идти в комплекте с другим ПО, например WebLogic, или даже можно использовать Jrockit JVM от Oracle (раньше BEA). Или другие JVM от IBM, Apple и др. Большинство этих «других» JVM работают по похожей на HotSpot схеме, за исключением Jrockit, чье управление памятью отлично от других и который, например, не имеет выделенного Permanent Generation (см. ниже).
Давайте начнём с того, как JVM использует память. В JVM память делится на два сегмента – Heap и Permanent Generation. На диаграмме Permanent Generation обозначено зеленым, остальное – heap.
The Permanent Generation
Permanent generation используется только JVM для хранения необходимых данных, в том числе метаданные о созданных объектах. При каждом создании объекта JVM будет «класть» некоторый набор данных в PG. Соответственно, чем больше вы создаете объектов разных типов, тем больше «жилого пространства» требуется в PG. Размер PG можно задать двумя параметрами JVM: -XX:PermSize – задаёт минимальный, или изначальный, размер PG, и -XX:MaxPermSize – задаёт максимальный размер. При запуске больших Java-приложений мы часто задаём одинаковые значения для этих параметров, так что PG создаётся сразу с размером «по-максимуму», что может увеличить производительность, так как изменение размера PG – дорогостоящая (трудоёмкая) операция. Определение одинаковых значений для этих двух параметров может избавить JVM от выполнения дополнительных операций, таких как проверки необходимости изменения размера PG и, естественно, непосредственного изменения.
Heap
Heap – основной сегмент памяти, где хранятся все ваши объекты. Heap делится на два подсегмента, Old Generation и New Generation. New Generation в свою очередь делится на Eden и два сегмента Survivor. Размер heap также можно указать параметрами. На диаграмме это Xms (минимум) и -Xmx (максимум). Дополнительные параметры контролируют размеры сегментов heap. Мы позднее посмотрим один из них, остальные за рамкой этого поста. При создании объекта, когда вы пишете что-то типа byte[] data = new byte[1024], этот объект создаётся в сегменте Eden. Новые объекты создаются в Eden. Кроме собственно данных для нашего массива байт здесь есть ещё ссылка (указатель) на эти данные. Дальнейшее объяснение упрощено. Когда вы хотите создать новый объект, но места в Eden уже нет, JVM проводит garbage collection, что значит, что JVM ищет в памяти все объекты, которые более не нужны, и избавляется от них.
Garbage collection – это круто! Если вы когда-либо программировали на языках типа C или Objective-C, то вы знаете, что ручное управление памятью – вещь утомительная и порой вызывающая ошибки. Наличие JVM, которая автоматически позаботится о неиспользуемых объектах, делает разработку проще и сокращает время отладки. Если же вы никогда не писали на подобных языках, значит возьмите С и попробуйте написать программу, и ощутите, как ценно то, что предоставляется вашим языком совершенно бесплатно.
Существует множество алгоритмов, которыми может воспользоваться JVM для проведения garbage collection. Можно указать, какие из них будут использоваться JVM с помощью параметров. Давайте посмотрим на пример. Допустим, у нас есть следующий код:
String a = "hello"; String b = "apple"; String c = "banana"; String d = "apricot"; String e = "pear"; // // делаем чего-нибудь // a = null; b = null; c = null; e = null;
В Eden создаётся («размещается») пять объектов, как показано пятью желтыми квадратиками на диаграмме. После «чего-нибудь» мы освобождаем a,b,c и e, присваивая ссылкам null. Имея в виду, что больше ссылок на них нет, они теперь не нужны, и показаны красным на второй диаграмме. При этом мы всё ещё нуждаемся в String d (показано зелёным).
Если мы попробуем разместить ещё объект, JVM обнаружит, что Eden полон и надо провести чистку. Самый простой алгоритм для garbage collection называется Copy Collection, и работает он так, как показано на диаграмме. На первом этапе Mark помечаются неиспользуемые объекты (красные). На втором (Copy) объекты, которые ещё нужны (d) копируется в сегмент survivor – квадрат справа. Сегментов Survivor два, и они меньше Eden. Теперь все объекты, которые мы хотим, чтобы они были сохранены, скопированы в Survivor, и JVM просто удаляет всё из Eden. На этом всё. Этот алгоритм создаёт кое-что, что называется моментом, «когда мир остановился». Во время выполнения GC все другие треды в JVM переводятся в состояние паузы, ради того, чтобы никакой из них не попробовал влезть в память после того, как мы скопировали всё оттуда, что привело бы к потере того, что сделано. Это не великая проблема, если она в небольшом приложении, но если у нас на руках серьёзная программа, скажем, с 8-гигабайтным heap, то выполнение GC займёт много времени – секунды или даже минуты. Естественно, что каждый раз останавливать приложение – вариант не подходящий. Потому и существуют другие алгоритмы, и используются часто. Copy Collection же работает хорошо в том случае, если у нас много мусора и мало полезных объектов. В этом посте мы поговорим насчёт двух распространённых алгоритмов. Для интересующихся есть много информации в Сети и несколько хороших книг. Следующий алгоритм называется Mark-Sweep-Compact Collection. У алгоритма три этапа:
1) «Mark»: помечаются неиспользуемые объекты (красные).
2) «Sweep»: эти объекты удаляются из памяти. Обратите внимание на пустые слоты на диаграмме.
3) «Compact»: объекты размещаются, занимая свободные слоты, что освобождает пространство на тот случай, если потребуется создать «большой» объект.
Но это всё теория, так что давайте посмотрим, как это работает, на примере. К счастью, JDK имеет визуальный инструмент для наблюдения за JVM в реальном времени, а именно jvisualvm. Лежит он прямо в bin JDK. Воспользуемся ею немного позже, сначала займёмся приложением. Для разработки, билдов и зависимостей я воспользовался Maven, но вам он не нужен – просто скомпилируйте и запустите приложение, если вам так угодно.
Я выбрал простой JAR (98) и все по умолчанию для остального. Дальше переключился в директорию memoryTool и отредактировал pom.xml (ниже, добавил блок plugin). Это позволило мне запустить приложение прямо из Maven, передав необходимые параметры.
При этом: -Xms определяем исходный/минимальный размер heap в 512 мб -Xmx определяем максимальный размер heap в 512 мб -XX:NewRatio определяем размер old generation большим в три раза чем размер new generation -XX:+PrintGCTimeStamps, -XX:+PrintGCDetails и -Xloggc:gc.log JVM печатает дополнительную информацию касаемо garbage collection в файл gc.log -classpath определяем classpath com.redstack.App main класс
Ниже – код main-класса. Простая программа, в которой мы создаём объекты и далее выкидываем их, так что понятно, сколько памяти используется и мы можем посмотреть, что происходит с JVM.
package com.redstack; import java.io.*; import java.util.*; public class App < private static List objects = new ArrayList(); private static boolean cont = true; private static String input; private static BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); public static void main(String[] args) throws Exception < System.out.println("Welcome to Memory Tool!"); while (cont) < System.out.println( "\n\nI have " + objects.size() + " objects in use, about " + (objects.size() * 10) + " MB." + "\nWhat would you like me to do?\n" + "1. Create some objects\n" + "2. Remove some objects\n" + "0. Quit"); input = in.readLine(); if ((input != null) && (input.length() >= 1)) < if (input.startsWith("0")) cont = false; if (input.startsWith("1")) createObjects(); if (input.startsWith("2")) removeObjects(); >> System.out.println("Bye!"); > private static void createObjects() < System.out.println("Creating objects. "); for (int i = 0; i < 2; i++) < objects.add(new byte[10*1024*1024]); >> private static void removeObjects() < System.out.println("Removing objects. "); int start = objects.size() - 1; int end = start - 2; for (int i = start; ((i >= 0) && (i > end)); i--) < objects.remove(i); >> >
Для сборки и выполнения кода используем следующую команду Maven: mvn package exec:exec
Как только скомпилируете и будете готовы к дальнейшим действиям, запускайте ее и jvisualvm. Если вы не использовали jvisualvm ранее, то нужно установить плагин VisualGC: выберите Plugins в меню Tools, дальше вкладку Available Plugins. Выберите Visual GC и нажмите Install. Вы должны увидеть список процессов JVM. Два раза кликните на том, в котором исполняется ваше приложение (в данном примере com.redstack.App) и откройте вкладку Visual GC. Должно появиться что-то типа того, что на скриншоте ниже.
Обратите внимание, что можно визуально наблюдать состояние permanent generation, old generation, eden и сегментов survivor (S0 и S1). Цветные колонки показывают используемую память. Справа находится historical view, которое показывает, когда JVM проводило garbage collections и количество памяти в каждом из сегментов.
В окне приложения начните создавать объекты (опция 1) и наблюдайте, что будет происходить в Visual GC. Обратите внимание на то, что новые объекты всегда создаются в eden. Теперь сделайте пару объектов ненужными (опция 2). Возможно, вы не увидите изменений в Visual GC. Это потому, что JVM не чистит это пространство до тех пор, пока не закончена процедура garbage collection.
Чтобы инициировать garbage collection, создайте ещё объектов, заполнив Eden. Обратите внимание, что произойдет в момент заполнения. Если в Eden много мусора, вы увидите, как объекты из Eden «переезжают» в survivor. Однако, если в Eden мало мусора, вы увидите, как объекты «переезжают» в old generation. Это случается тогда, когда объекты, которые необходимо оставить, больше чем survival. Пронаблюдайте также за постепенным увеличением Permanent Generation. Попробуйте заполнить Eden, но не до конца, потом выбросьте почти все объекты, оставьте только 20 мб. Получиться так, что Eden на большую часть заполнен мусором. После этого создайте ещё объектов. На этот раз вы увидите, что объекты из Eden переходят в Survivor.
А теперь давайте посмотрим, что будет, если у нас не хватит памяти. Создавайте объекты до тех пор, пока их не будет на 460 мб. Обратите внимание, что и Eden и Old Generation заполнены практически полностью. Создайте ещё пару объектов. Когда больше не останется памяти, приложение «упадёт» с исключением OutOfMemoryException. Вы уже могли сталкиваться с подобным поведением и думать, почему же это произошло – особенно, если у вас большой объем физической памяти на компьютере и вы удивлялись, как такое вообще могло произойти, что не хватает памяти – теперь вы знаете, почему. Если так случится, что Permanent Generation заполнится (довольно сложно в случае нашего примера добиться этого), у вас будет брошено другое исключение, сообщающее, что PermGen заполнен.
И, наконец, ещё один способ посмотреть, что происходило, это обратиться к логу. Вот немного из моего:
В логе можно увидеть, что происходило в JVM — обратите внимание, что использовался алгоритм Concurrent Mark Sweep Compact Collection algorithm (CMS), есть описание этапов и YG — Young Generation.
Можно воспользоваться этими настройками и в «продакшене». Есть даже инструменты, визуализирующие лог.
Ну, вот и закончилось наше краткое введение в теории и практики JVM garbage collection. Надеюсь, что приложение из примера помогло вам чётко представить, что происходит в JVM когда запущено ваше приложение. Спасибо Rupesh Ramachandran за то, чему он научил меня относительно настроек JVM и garbage collection.