«1. Введение
В этом руководстве мы рассмотрим основные проблемы, связанные с управлением памятью в Java, и необходимость постоянного поиска лучших способов добиться этого. В первую очередь речь пойдет о новом экспериментальном сборщике мусора, представленном в Java, который называется Shenandoah, и о том, как он сравнивается с другими сборщиками мусора.
2. Понимание проблем при сборке мусора
Сборщик мусора — это форма автоматического управления памятью, при которой среда выполнения, такая как JVM, управляет выделением и освобождением памяти для пользовательских программ, работающих на ней. Существует несколько алгоритмов реализации сборщика мусора. К ним относятся подсчет ссылок, маркировка-очистка, маркировка-сжатие и копирование.
2.1. Рекомендации по сборщику мусора
В зависимости от алгоритма, который мы используем для сборки мусора, он может либо работать, когда пользовательская программа приостановлена, либо работать одновременно с пользовательской программой. Первый обеспечивает более высокую пропускную способность за счет высокой задержки из-за длительных пауз, также известных как паузы «останови мир». Последний нацелен на лучшую задержку, но снижает пропускную способность.
На самом деле, большинство современных коллекционеров используют гибридную стратегию, в которой они применяют как остановку мира, так и параллельный подход. Обычно это работает путем разделения пространства кучи на молодое и старое поколения. Затем сборщики поколений используют сбор с остановкой мира в молодом поколении и параллельный сбор в старом поколении, возможно, постепенно, чтобы уменьшить паузы.
Тем не менее, лучше всего найти сборщик мусора, который работает с минимальными паузами и обеспечивает высокую пропускную способность — и все это с предсказуемым поведением при размере кучи, который может варьироваться от маленького до очень большого! Это постоянная борьба, которая поддерживала темп инноваций в сборке мусора Java с первых дней.
2.2. Существующие сборщики мусора в Java
Некоторые из традиционных сборщиков мусора включают последовательные и параллельные сборщики. Они являются сборщиками поколений и используют копирование в молодом поколении и маркировку-компакт в старом поколении:
Обеспечивая хорошую пропускную способность, они страдают от проблемы длительных остановок мира.
Сборщик Concurrent Mark Sweep (CMS), представленный в Java 1.4, представляет собой параллельный сборщик с малой паузой. Он работает с копированием в молодом поколении и меткой-чисткой в старом поколении:
Он пытается минимизировать время паузы, выполняя большую часть работы одновременно с пользовательской программой. Тем не менее, он по-прежнему имеет проблемы, приводящие к непредсказуемым паузам, требует больше процессорного времени и не подходит для кучи размером более 4 ГБ.
В качестве долгосрочной замены CMS в Java 7 был представлен сборщик Garbage First (G1). G1 — это сборщик с поколенческим, параллельным, параллельным и инкрементально уплотняемым сборщиком с малой паузой. Он работает с копированием в молодом поколении и mark-compact в старом поколении:
G1, однако, также является региональным сборщиком и структурирует область кучи на более мелкие регионы. Это дает ему преимущество более предсказуемых пауз. Ориентированная на многопроцессорные машины с большим объемом памяти, G1 также не свободна от пауз.
Итак, гонка за лучшим сборщиком мусора продолжается, особенно тот, который еще больше сокращает время паузы. В последнее время JVM представила ряд экспериментальных сборщиков, таких как Z, Epsilon и Shenandoah. Кроме того, G1 продолжает улучшаться.
Цель состоит в том, чтобы максимально приблизиться к Java без пауз!
3. Сборщик мусора Shenandoah
Shenandoah — это экспериментальный сборщик мусора, который был представлен в Java 12 и позиционируется как специалист по задержкам. Он пытается сократить время пауз, выполняя больше работы по сборке мусора одновременно с пользовательской программой.
«Например, Shenendoah пытается одновременно перемещать и сжимать объекты. По сути это означает, что время паузы в Shenandoah больше не прямо пропорционально размеру кучи. Следовательно, он может обеспечить последовательное поведение с низкой паузой, независимо от размера кучи.
3.1. Структура кучи
Shenandoah, как и G1, является региональным коллектором. Это означает, что он делит область кучи на набор областей одинакового размера. Область — это, по сути, единица выделения или освобождения памяти:
Но, в отличие от G1 и других коллекторов поколений, Shenandoah не делит область кучи на поколения. Следовательно, он должен маркировать большинство живых объектов каждый цикл, чего могут избежать коллекционеры поколений.
3.2. Макет объекта
В Java объекты в памяти содержат не только поля данных — они также несут некоторую дополнительную информацию. Эта дополнительная информация состоит из заголовка, содержащего указатель на класс объекта, и слова-метки. Слово метки можно использовать по-разному, например указатели переадресации, возрастные биты, блокировка и хэширование:
Shenandoah добавляет дополнительное слово в этот макет объекта. Это служит косвенным указателем и позволяет Shenandoah перемещать объекты без обновления всех ссылок на них. Это также известно как указатель Брукса.
3.3. Барьеры
Выполнение цикла сбора в режиме остановки мира проще, но сложность возрастает, когда мы делаем это одновременно с пользовательской программой. Это создает различные проблемы на этапах сбора, такие как одновременная маркировка и уплотнение.
Решение заключается в перехвате всех обращений к куче через то, что мы называем барьерами. Shenandoah и другие параллельные сборщики, такие как G1, используют барьеры для обеспечения согласованности кучи. Однако барьеры являются дорогостоящими операциями и, как правило, снижают пропускную способность коллектора.
Например, операции чтения и записи объекта могут быть перехвачены сборщиком с помощью барьеров:
Shenandoah использует несколько барьеров на разных этапах, таких как барьер SATB, барьер чтения и барьер записи. Мы увидим, где они используются в следующих разделах.
3.4. Режимы, эвристики и режимы отказов
Режимы определяют то, как работает Шенандоа, например, какие барьеры он использует, а также определяют его рабочие характеристики. Доступны три режима: нормальный/SATB, iu и пассивный. Режим нормальный/SATB используется по умолчанию.
Эвристика определяет, когда должна начинаться коллекция и какие регионы она должна включать. К ним относятся адаптивный, статический, компактный и агрессивный, причем адаптивный используется в качестве эвристики по умолчанию. Например, он может выбрать регионы с 60 или более процентами мусора и начать цикл сбора, когда будет выделено 75 процентов регионов.
Shenandoah нужно собирать кучу быстрее, чем пользовательская программа, которая ее выделяет. Но иногда он может отставать, что приводит к одному из режимов отказа. Эти режимы сбоя включают стимуляцию, вырожденный сбор и, в худшем случае, полный сбор.
4. Фазы сбора Шенандоа
Цикл сбора Шенандоа состоит в основном из трех этапов: пометка, эвакуация и обновление ссылок. Хотя большая часть работы на этих этапах выполняется одновременно с пользовательской программой, все же есть небольшие части, которые должны выполняться в режиме остановки мира.
4.1. Маркировка
Маркировка — это процесс идентификации всех объектов в куче или ее частей, которые недоступны. Мы можем сделать это, начав с корневых объектов и пройдя граф объектов, чтобы найти достижимые объекты. При обходе мы также присваиваем каждому объекту один из трех цветов: белый, серый или черный:
Маркировка в режиме остановки мира проще, но в параллельном режиме она усложняется. Это связано с тем, что программа пользователя одновременно изменяет граф объекта во время выполнения маркировки. Shenandoah решает эту проблему с помощью алгоритма Snapshot At the Beginning (SATB).
«Это означает, что любой объект, который был активен в начале разметки или был выделен с начала разметки, считается живым. Shenandoah использует барьер SATB для сохранения представления SATB о куче.
Хотя большая часть маркировки выполняется одновременно, некоторые части все еще выполняются в режиме остановки мира. Части, которые происходят в режиме остановки мира, — это метка инициализации для сканирования корневого набора и конечная метка для очистки всех ожидающих очередей и повторного сканирования корневого набора. Окончательная отметка также подготавливает набор для сбора, который указывает области, подлежащие эвакуации.
4.2. Очистка и эвакуация
После того, как маркировка завершена, области мусора готовы к утилизации. Мусорные области — это области, в которых нет живых объектов. Очистка происходит одновременно.
Теперь следующий шаг — переместить живые объекты из набора коллекций в другие регионы. Это сделано для уменьшения фрагментации при распределении памяти и, следовательно, также известно как компактный. Эвакуация или уплотнение происходит полностью одновременно.
Вот чем Шенандоа отличается от других коллекционеров. Одновременное перемещение объектов затруднено, поскольку пользовательская программа продолжает их читать и записывать. Шенандоа удается добиться этого, выполняя операцию сравнения и замены указателя Брукса объекта, чтобы указать на его версию в пространстве:
Кроме того, Шенандоа использует барьеры чтения и записи, чтобы гарантировать, что -space’ инвариант сохраняется во время одновременной эвакуации. Это означает, что чтение и запись должны происходить из пространства-то, которое гарантированно выдержит эвакуацию.
4.3. Обновление ссылки
Эта фаза в цикле сбора предназначена для обхода кучи и обновления ссылок на объекты, которые были перемещены во время эвакуации:
Фаза обновления ссылки, опять же, в основном выполняется одновременно. Существуют короткие периоды init-update-refs, которые инициализируют фазу ссылки на обновление, и final-update-refs, которые повторно обновляют корневой набор и перезапускают регионы из набора сбора. Только для этого требуется режим остановки мира.
5. Сравнение с другими экспериментальными сборщиками
Shenandoah — не единственный экспериментальный сборщик мусора, недавно появившийся в Java. Другие включают Z и Эпсилон. Давайте поймем, как они сравниваются с Шенандоа.
5.1. Z Collector
Представленный в Java 11 сборщик Z представляет собой сборщик одного поколения с малой задержкой, предназначенный для очень больших размеров кучи — мы говорим о многотерабайтной территории. Сборщик Z выполняет большую часть своей работы одновременно с пользовательской программой и использует барьер нагрузки для ссылок на кучу.
Кроме того, сборщик Z использует преимущества 64-битных указателей с помощью метода, называемого раскрашиванием указателей. Здесь цветные указатели хранят дополнительную информацию об объектах в куче. Сборщик Z переназначает объекты, используя дополнительную информацию, хранящуюся в указателе, чтобы уменьшить фрагментацию памяти.
Вообще говоря, цели коллекционера Z аналогичны целям Шенандоа. Оба они нацелены на достижение малого времени паузы, которое не прямо пропорционально размеру кучи. Тем не менее, с Shenandoah доступно больше вариантов настройки, чем с Z-коллектором.
5.2. Сборщик Epsilon
Epsilon, также представленный в Java 11, имеет совершенно другой подход к сборке мусора. По сути, это пассивный или «нерабочий» сборщик, что означает, что он обрабатывает выделение памяти, но не перерабатывает ее! Итак, когда в куче заканчивается память, JVM просто выключается.
Но зачем нам вообще использовать такой сборщик? По сути, любой сборщик мусора косвенно влияет на производительность пользовательской программы. Очень сложно протестировать приложение и понять влияние на него сборки мусора.
«Эпсилон служит именно этой цели. Он просто устраняет влияние сборщика мусора и позволяет нам запускать приложение изолированно. Но это требует от нас очень четкого понимания требований к памяти для нашего приложения. Следовательно, мы можем добиться большей производительности приложения.
Очевидно, что у Эпсилон совсем другая цель, чем у Шенандоа.
6. Заключение
В этой статье мы рассмотрели основы сборки мусора в Java и необходимость ее постоянного улучшения. Мы подробно обсудили самый последний экспериментальный сборщик, представленный на Java — Shenandoah. Мы также рассмотрели, как он работает по сравнению с другими экспериментальными сборщиками, доступными на Java.
Погоня за универсальным сборщиком мусора не осуществится в ближайшее время! Таким образом, хотя G1 остается сборщиком по умолчанию, эти новые дополнения дают нам возможность использовать Java в ситуациях с малой задержкой. Тем не менее, мы не должны рассматривать их как прямую замену другим сборщикам с высокой пропускной способностью.