JEP 439: Generational ZGC
ZGC с поколениями
| Ответственный | Stefan Karlsson |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Delivered |
| Выпуск | 21 |
| Компонент | hotspot / gc |
| Обсуждение | hotspot dash gc dash dev at openjdk dot org |
| Трудоёмкость | XL |
| Длительность | XL |
| Связан с | JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production) |
| JEP 474: ZGC: Generational Mode by Default | |
| JEP 490: ZGC: Remove the Non-Generational Mode | |
| Рецензенты | Erik Helin, Erik Österlund, Vladimir Kozlov |
| Одобрен | Vladimir Kozlov |
| Создан | 2021/08/25 12:01 |
| Обновлён | 2024/10/07 17:28 |
| Задача | 8272979 |
Аннотация
Повысить производительность приложений, расширив сборщик мусора Z Garbage Collector (ZGC) так, чтобы он поддерживал отдельные поколения для молодых и старых объектов. Так ZGC сможет чаще собирать молодые объекты, которые, как правило, умирают молодыми.
Цели
Приложения, работающие с Generational ZGC (ZGC с поколениями), должны получить
- меньшие риски остановок при выделении памяти (allocation stalls),
- меньшие накладные расходы памяти, необходимой для кучи, и
- меньшие накладные расходы процессора на сборку мусора.
Эти преимущества должны достигаться без значительного снижения пропускной способности по сравнению с ZGC без поколений. Основные свойства ZGC без поколений должны сохраниться:
- время паузы не должно превышать 1 миллисекунду,
- должны поддерживаться размеры кучи от нескольких сотен мегабайт до многих терабайт и
- ручная настройка должна быть минимальной.
В качестве примеров к последнему пункту: не должно быть необходимости вручную настраивать
- размер поколений,
- число потоков, используемых сборщиком мусора, или
- как долго объекты должны находиться в молодом поколении.
Наконец, Generational ZGC должен стать для большинства сценариев использования лучшим решением, чем ZGC без поколений. Со временем у нас должна появиться возможность заменить второй вариант первым, чтобы снизить долгосрочные затраты на сопровождение.
Что не является целью
Выполнение обработки ссылок в молодом поколении не является целью.
Мотивация
ZGC (JEP 333) спроектирован для низкой задержки и высокой масштабируемости. Он доступен для промышленного использования начиная с JDK 15 (JEP 377).
Большую часть своей работы ZGC выполняет, пока работают потоки приложения, и приостанавливает эти потоки лишь ненадолго. Время пауз ZGC стабильно измеряется микросекундами; для сравнения, время пауз сборщика мусора по умолчанию, G1, составляет от миллисекунд до секунд. Малое время пауз ZGC не зависит от размера кучи: рабочие нагрузки могут использовать кучу размером от нескольких сотен мегабайт вплоть до нескольких терабайт и при этом сохранять малое время пауз.
Для многих рабочих нагрузок достаточно просто использовать ZGC, чтобы решить все проблемы с задержками, связанные со сборкой мусора. Это хорошо работает, пока доступно достаточно ресурсов (т. е. памяти и процессора), чтобы ZGC мог освобождать память быстрее, чем её потребляют одновременно работающие потоки приложения. Однако сейчас ZGC хранит все объекты вместе, независимо от их возраста, поэтому при каждом запуске он должен собирать все объекты.
Слабая гипотеза о поколениях гласит, что молодые объекты, как правило, умирают молодыми, а старые объекты, как правило, остаются. Поэтому сборка молодых объектов требует меньше ресурсов и освобождает больше памяти, а сборка старых объектов требует больше ресурсов и освобождает меньше памяти. Значит, мы можем повысить производительность приложений, использующих ZGC, если будем чаще собирать молодые объекты.
Описание
Включение Generational ZGC
Чтобы переход прошёл плавно, сначала мы сделаем Generational ZGC доступным наряду с ZGC без поколений. Параметр командной строки -XX:+UseZGC будет выбирать ZGC без поколений; чтобы выбрать Generational ZGC, добавьте параметр -XX:+ZGenerational:
$ java -XX:+UseZGC -XX:+ZGenerational ...
В одном из будущих выпусков мы намерены сделать Generational ZGC вариантом по умолчанию, и тогда -XX:-ZGenerational будет выбирать ZGC без поколений. В ещё более позднем выпуске мы намерены удалить ZGC без поколений, и тогда параметр ZGenerational станет устаревшим.
Архитектура
Generational ZGC делит кучу на два логических поколения: молодое поколение предназначено для недавно выделенных объектов, а старое поколение — для долгоживущих объектов. Каждое поколение собирается независимо от другого, поэтому ZGC может сосредоточиться на сборке молодых объектов, которая приносит больше выгоды.
Как и в ZGC без поколений, вся сборка мусора выполняется конкурентно, пока приложение работает, а паузы приложения обычно короче одной миллисекунды. Поскольку ZGC читает и изменяет граф объектов одновременно с приложением, он должен следить за тем, чтобы приложение видело граф объектов в согласованном состоянии. ZGC добивается этого с помощью цветных указателей (colored pointers), барьеров загрузки (load barriers) и барьеров записи (store barriers).
-
Цветной указатель — это указатель на объект в куче, который наряду с адресом объекта в памяти содержит метаданные, кодирующие известное состояние объекта. Метаданные описывают, известно ли, что объект жив, корректен ли адрес и т. д. ZGC всегда использует 64-битные указатели на объекты и поэтому может вместить биты метаданных и адреса объектов для куч размером до многих терабайт. Когда поле объекта ссылается на другой объект, ZGC реализует эту ссылку с помощью цветного указателя.
-
Барьер загрузки — это фрагмент кода, который ZGC внедряет в приложение везде, где приложение читает поле объекта, ссылающееся на другой объект. Барьер загрузки интерпретирует метаданные цветного указателя, хранящегося в поле, и, возможно, выполняет некоторое действие, прежде чем приложение использует объект, на который указывает ссылка.
ZGC без поколений использует и цветные указатели, и барьеры загрузки. Generational ZGC также использует барьеры записи, чтобы эффективно отслеживать ссылки из объектов одного поколения на объекты другого поколения.
- Барьер записи — это фрагмент кода, который ZGC внедряет в приложение везде, где приложение сохраняет ссылки в поля объектов. Generational ZGC добавляет к цветным указателям новые биты метаданных, чтобы барьеры записи могли определить, было ли записываемое поле уже зарегистрировано как потенциально содержащее межпоколенческий указатель. Благодаря цветным указателям барьеры записи Generational ZGC эффективнее традиционных барьеров записи в сборщиках с поколениями.
Добавление барьеров записи позволяет Generational ZGC перенести работу по маркировке достижимых объектов из барьеров загрузки в барьеры записи. То есть барьеры записи могут с помощью битов метаданных в цветных указателях эффективно определять, нужно ли маркировать объект, на который поле ссылалось до записи.
Вынос маркировки из барьеров загрузки упрощает их оптимизацию, а это важно, поскольку барьеры загрузки часто выполняются чаще, чем барьеры записи. Теперь, интерпретируя цветной указатель, барьер загрузки должен лишь обновить адрес объекта, если объект был перемещён, и обновить метаданные, чтобы отметить, что адрес заведомо корректен. Последующие барьеры загрузки будут интерпретировать эти метаданные и не станут снова проверять, был ли объект перемещён.
Generational ZGC использует в цветных указателях отдельные наборы битов метаданных для маркировки и для перемещения, чтобы поколения можно было собирать независимо.
В остальных разделах описаны важные концепции архитектуры, которые отличают Generational ZGC от ZGC без поколений и от других сборщиков мусора:
- Без многократно отображённой памяти
- Оптимизированные барьеры
- Запомненные множества (remembered sets) с двойной буферизацией
- Перемещение без дополнительной памяти кучи
- Плотные регионы кучи
- Большие объекты
- Полные сборки мусора
Без многократно отображённой памяти
ZGC без поколений использует многократно отображённую память, чтобы снизить накладные расходы барьеров загрузки. Generational ZGC вместо этого использует явный код в барьерах загрузки и записи.
Для пользователей главное преимущество этого изменения в том, что становится проще измерить объём памяти, используемой кучей. При многократном отображении одна и та же память кучи отображается в три отдельных диапазона виртуальных адресов, поэтому использование кучи, о котором сообщают такие инструменты, как ps, примерно втрое превышает фактически используемый объём памяти.
Для самого сборщика мусора это изменение означает, что биты метаданных цветного указателя больше не обязаны находиться в той части указателя, которая соответствует диапазону доступных адресов памяти кучи. Это позволяет добавить больше битов метаданных, а также открывает возможность увеличить максимальный размер кучи сверх предела ZGC без поколений в 16 терабайт.
В Generational ZGC ссылки на объекты, хранящиеся в полях объектов, реализованы как цветные указатели. Однако ссылки на объекты, хранящиеся в стеке JVM, реализованы как бесцветные указатели, без битов метаданных, в аппаратном стеке или в регистрах процессора. Барьеры загрузки и записи преобразуют цветные указатели в бесцветные и обратно.
Поскольку цветные указатели никогда не встречаются в аппаратном стеке или в регистрах процессора, можно использовать более необычную раскладку цветного указателя, если только преобразование между цветными и бесцветными указателями выполняется эффективно. Раскладка цветного указателя в Generational ZGC помещает метаданные в младшие биты указателя, а адрес объекта — в старшие биты. Это минимизирует число машинных инструкций в барьере загрузки. При аккуратном кодировании адреса памяти и битов метаданных одна инструкция сдвига (на x64) может и проверить, требует ли указатель обработки, и удалить биты метаданных.
Оптимизированные барьеры
С появлением барьеров записи и новых обязанностей у барьеров загрузки больше кода сборщика мусора будет перемешано со скомпилированным кодом приложения. Чтобы максимизировать пропускную способность, барьеры должны быть очень хорошо оптимизированы. Многие ключевые архитектурные решения Generational ZGC касаются схемы цветных указателей и барьеров.
Некоторые из приёмов, используемых для оптимизации барьеров:
- Быстрые и медленные пути
- Минимизация обязанностей барьеров загрузки
- Барьеры запомненного множества
- Барьеры маркировки SATB
- Объединённые проверки в барьерах записи
- Буферы барьеров записи
- Патчинг барьеров
Быстрые и медленные пути
ZGC разделяет барьеры на две части. Быстрый путь проверяет, нужно ли выполнить дополнительную работу сборщика мусора, прежде чем приложение использует объект, на который указывает ссылка. Медленный путь выполняет эту дополнительную работу. Проверку быстрого пути выполняют все обращения к объектам. Как следует из названия, он должен быть быстрым, поэтому этот код вставляется непосредственно в код приложения, скомпилированный JIT-компилятором. Медленный путь выполняется лишь в небольшой доле случаев. Когда выполняется медленный путь, цвет указателя на объект, к которому идёт обращение, меняется, так что последующие обращения по тому же указателю в течение некоторого времени снова не приводят к медленному пути. Поэтому высокая оптимизация медленных путей менее важна. Для удобства сопровождения они реализованы как функции C++ в JVM.
Именно так разделены барьеры загрузки в ZGC без поколений. В Generational ZGC та же схема применяется к барьерам записи и связанной с ними работе сборщика мусора.
Минимизация обязанностей барьеров загрузки
В ZGC без поколений барьер загрузки отвечает за
- обновление устаревших указателей на объекты, перемещённые сборщиком мусора, и
- маркировку загружаемых объектов как живых — приложение загружает объект, поэтому он считается живым.
В Generational ZGC мы должны отслеживать два поколения и преобразовывать цветные указатели в бесцветные и обратно. Чтобы уменьшить сложность и позволить оптимизировать быстрый путь барьера загрузки, ответственность за маркировку переносится на барьеры записи.
В Generational ZGC барьеры загрузки отвечают за
- удаление битов метаданных из цветных указателей и
- обновление устаревших указателей на объекты, перемещённые сборщиком мусора.
Барьеры записи отвечают за
- добавление битов метаданных для создания цветных указателей,
- поддержку запомненного множества (remembered set), которое отслеживает указатели из объектов старого поколения на объекты молодого поколения, и
- маркировку объектов как живых.
Барьеры запомненного множества
Когда Generational ZGC собирает молодое поколение, он посещает только объекты молодого поколения. Однако объекты старого поколения могут содержать поля, указывающие на объекты молодого поколения, т. е. указатели из старого поколения в молодое. Эти поля необходимо посещать при сборке молодого поколения по двум причинам.
-
Корни маркировки сборщика мусора — такое поле может содержать единственную ссылку, благодаря которой часть графа объектов молодого поколения остаётся достижимой. Сборщик мусора должен рассматривать эти поля как корни графа объектов, чтобы гарантировать, что все живые объекты будут найдены и помечены как живые.
-
Устаревшие указатели в старом поколении — при сборке молодого поколения объекты перемещаются, но указатели на эти объекты не обновляются сразу. Вместо этого указатели обновляются лениво, барьерами загрузки, когда приложение на них натыкается. В какой-то момент GC должен обновить все устаревшие указатели из старого поколения в молодое, на которые приложение так и не наткнулось.
Множество указателей из старого поколения в молодое называется запомненным множеством (remembered set). Запомненное множество содержит все адреса памяти в старом поколении, которые могут содержать указатели на объекты в молодом поколении. Записи в запомненное множество добавляют барьеры записи. Всякий раз, когда ссылка сохраняется в поле объекта, считается, что это поле может содержать указатель из старого поколения в молодое. Медленный путь барьера записи отфильтровывает записи в поля молодого поколения, поскольку интерес представляют только адреса в старом поколении. Медленный путь не фильтрует по значению, записанному в поле, которое может ссылаться как на молодое, так и на старое поколение. GC проверяет текущее значение поля объекта, когда использует запомненное множество.
Всё это обеспечивает свойство однократного срабатывания (act-once property) барьеров записи в части ведения запомненного множества. Это значит, что между началом двух последовательных фаз маркировки молодого поколения медленный путь барьера записи выполняется только один раз для каждого поля объекта, в которое производится запись. При первой записи в поле происходит следующее:
- Быстрый путь проверяет значение поля, которое будет перезаписано,
- Цвет показывает, что в это поле не было записи с предыдущей фазы маркировки молодого поколения, поэтому
- Выполняется медленный путь,
- Адрес поля, в которое производится запись, добавляется в запомненное множество, и
- Новое значение указателя окрашивается и сохраняется в поле.
Новое значение указателя окрашивается так, чтобы последующие проверки на быстром пути видели, что это поле объекта уже прошло через медленный путь.
Барьеры маркировки SATB
В отличие от ZGC без поколений, Generational ZGC использует алгоритм маркировки snap shot at the beginning (SATB, снимок в начале). В начале фазы маркировки GC делает снимок корней GC; к концу фазы маркировки гарантированно будут найдены и помечены как живые все объекты, которые были достижимы из этих корней на момент начала маркировки.
Для этого GC должен получать уведомления, когда ссылки между объектами в графе объектов разрываются. Поэтому барьеры записи сообщают GC значения полей, которые будут перезаписаны; затем GC помечает объект, на который указывает ссылка, а также обходит и помечает объекты, достижимые из него.
Барьерам записи достаточно сообщать о значении поля, которое будет перезаписано, только при первой записи в это поле в пределах цикла маркировки. Последующие записи в то же поле будут заменять только значения, которые GC и так гарантированно найдёт благодаря свойству SATB. Свойство SATB, в свою очередь, поддерживает свойство однократного срабатывания барьеров записи в части маркировки.
Объединённые проверки в барьерах записи
Функции барьеров записи по ведению запомненного множества и по маркировке во многом схожи. Обе используют проверки окрашенных указателей на быстром пути и соответствующие свойства однократного срабатывания. Вместо отдельных проверок на быстром пути для каждого условия мы объединяем их в одну общую проверку. Если нарушается любое из двух свойств, выполняется медленный путь и проделывается необходимая работа GC.
Буферы барьеров записи
Разделение барьеров на быстрые и медленные пути и окрашивание указателей сокращают число вызовов функций медленного пути на C++. Generational ZGC ещё больше снижает накладные расходы, размещая между быстрым и медленным путём JIT-компилируемый средний путь. Средний путь сохраняет значение, которое будет перезаписано, и адрес поля объекта в буфер барьера записи и возвращается в скомпилированный код приложения, не выполняя дорогостоящий медленный путь. Медленный путь выполняется только тогда, когда буфер барьера записи заполнен. Так распределяется часть накладных расходов на переход из скомпилированного кода приложения в код медленного пути на C++.
Патчинг барьеров
И барьеры загрузки, и барьеры записи выполняют проверки по значениям глобальных или локальных для потока переменных, которые GC изменяет при переходе в новую фазу. Прочитать эти переменные в барьерах можно по-разному, и накладные расходы на это различаются на разных архитектурах процессоров.
В Generational ZGC мы снижаем эти накладные расходы, по возможности модифицируя (патча) код барьеров. Глобальные значения кодируются в машинных инструкциях барьеров как непосредственные значения. Благодаря этому не нужно разыменовывать глобальную или локальную для потока переменную, чтобы получить текущее значение. Непосредственные значения патчатся при первом вызове методов после смены фазы GC, например, когда GC начинает фазу маркировки молодого поколения. Это дополнительно снижает накладные расходы барьеров.
Запомненные множества (remembered sets) с двойной буферизацией
Многие GC отслеживают межпоколенческие указатели с помощью техники запомненного множества, которая называется маркировкой таблицы карт (card table marking). Когда поток приложения записывает в поле объекта, он также записывает (то есть загрязняет) байт в большом массиве байтов, называемом таблицей карт (card table). Как правило, один байт таблицы соответствует диапазону адресов размером 512 байт в куче. Чтобы найти все указатели на объекты из старого поколения в молодое, GC должен найти и обойти все поля объектов в диапазонах адресов, соответствующих загрязнённым байтам таблицы карт.
Generational ZGC, напротив, точно записывает расположение полей объектов с помощью битовых карт, в которых каждый бит соответствует одному возможному адресу поля объекта. У каждого региона старого поколения есть пара битовых карт запомненного множества. Одна из битовых карт активна и заполняется потоками приложения при выполнении их барьеров записи, а другую GC использует как доступную только для чтения копию всех записанных полей объектов старого поколения, которые могут указывать на объекты в молодом поколении. Эти две битовые карты атомарно меняются местами каждый раз, когда начинается сборка молодого поколения. Одно из преимуществ такого подхода в том, что потокам приложения не нужно ждать очистки битовых карт. GC обрабатывает, а затем очищает одну из битовых карт, пока другая параллельно заполняется потоками приложения. Ещё одно преимущество: поскольку потоки приложения и потоки GC работают с разными битовыми картами, не нужны дополнительные барьеры памяти между этими двумя типами потоков. Другим сборщикам с поколениями, использующим маркировку таблицы карт, например G1, при маркировке карт требуется барьер памяти (memory fence), из-за чего производительность барьера записи может быть хуже.
Перемещение без дополнительной памяти кучи
Сборки молодого поколения в других GC HotSpot используют модель копирующей сборки (scavenging), в которой живые объекты находятся и перемещаются за один проход. Все объекты молодого поколения должны быть перемещены до того, как GC получит полные сведения о том, какие объекты были живы. GC, использующие эту модель, могут освободить память только после перемещения всех объектов. Поэтому таким GC приходится угадывать объём памяти, необходимый для выживших объектов, и обеспечивать наличие этого объёма памяти к моменту запуска GC. Если догадка неверна, требуется более дорогая операция очистки, например закрепление неперемещённых объектов на месте, что приводит к фрагментации, или полная сборка мусора с остановкой всех потоков приложения.
Generational ZGC использует два прохода: первый обходит и помечает все достижимые объекты, а второй перемещает помеченные объекты. Поскольку до начала фазы перемещения у GC есть полная информация о живости объектов, он может разбить работу по перемещению с гранулярностью в один регион. Как только все живые объекты перемещены из региона, то есть регион эвакуирован, этот регион можно повторно использовать как новый целевой регион для перемещений или для выделения памяти потоками приложения. Даже когда свободных регионов для перемещения объектов больше нет, ZGC может продолжать работу, уплотняя объекты в регионы, перемещаемые в данный момент. Благодаря этому Generational ZGC может перемещать и уплотнять молодое поколение без использования дополнительной памяти кучи.
Плотные регионы кучи
При перемещении объектов из молодого поколения число живых объектов и объём занимаемой ими памяти будут различаться от региона к региону. Например, в регионах, выделенных недавно, скорее всего, будет больше живых объектов.
ZGC анализирует плотность регионов молодого поколения, чтобы определить, какие регионы стоит эвакуировать, а какие либо слишком заполнены, либо слишком дороги для эвакуации. Регионы, не выбранные для эвакуации, стареют на месте: их объекты остаются там, где были, а сами регионы либо остаются в молодом поколении как регионы выживших, либо переводятся в старое поколение. Объекты в выживших регионах получают второй шанс умереть в расчёте на то, что к началу следующей сборки молодого поколения умрёт достаточно объектов, чтобы больше таких регионов стали пригодны для эвакуации.
Такой способ старения плотных регионов на месте уменьшает объём работы, необходимой для сборки молодого поколения.
Большие объекты
ZGC и так хорошо справляется с большими объектами. Благодаря отделению виртуальной памяти от физической и резервированию виртуальной памяти с запасом ZGC обычно удаётся избегать проблем с фрагментацией, из-за которых при использовании G1 иногда бывает трудно выделить память под большие объекты.
В Generational ZGC мы идём дальше и разрешаем выделять большие объекты в молодом поколении. Поскольку регионы могут стареть без перемещения, нет необходимости выделять большие объекты в старом поколении только ради того, чтобы избежать дорогих перемещений. Вместо этого они могут собираться в молодом поколении, если живут недолго, или дёшево переводиться в старое поколение, если живут долго.
Полные сборки мусора
При сборке старого поколения будут существовать указатели из объектов молодого поколения на объекты старого поколения. Эти указатели считаются корнями графа объектов старого поколения. Объекты молодого поколения часто изменяются, поэтому указатели из молодого поколения в старое не отслеживаются. Вместо этого такие указатели находятся с помощью сборки молодого поколения, выполняемой вместе с фазой маркировки старого поколения. Когда эта сборка молодого поколения находит указатели в старое поколение, она передаёт их процессу маркировки старого поколения.
Эта дополнительная сборка молодого поколения всё равно выполняется как обычная сборка молодого поколения и оставляет живые объекты в выживших регионах. Одно из следствий этого в том, что на выжившие объекты молодого поколения не распространяются обработка ссылок и выгрузка классов, выполняемые при сборке старого поколения. Это может заметить приложение, которое, например, освобождает последнюю ссылку на граф объектов, вызывает System.gc() и затем ожидает, что какая-то слабая ссылка будет очищена или поставлена в очередь или какой-то класс будет выгружен. Чтобы смягчить это, когда сборка мусора явно запрашивается кодом приложения, сначала, до начала сборки старого поколения, выполняется дополнительная сборка молодого поколения, чтобы перевести все выжившие объекты в старое поколение.
Альтернативы
Более простые схемы барьеров и окрашивания указателей
Текущую реализацию барьеров загрузки и сохранения непросто понять. Более простую версию было бы легче сопровождать, но ценой более дорогих барьеров загрузки и сохранения. Мы оценили около десяти разных реализаций барьеров, и ни одна не оказалась такой же производительной, как выбранный барьер загрузки на основе сдвига. Возможно, всё же стоит продолжить изучение и анализ этого компромисса между производительностью и сложностью.
Продолжить использовать многократно отображённую память
Можно было бы обойтись без схемы с неокрашенными корнями и использовать более простое решение с многократно отображённой памятью. Если в указателях требуется больше битов метаданных, чем в ZGC без поколений, то максимальный размер кучи будет ограничен. Другим подходом могло бы стать гибридное решение, в котором часть битов использует многократно отображённую память, а другие биты удаляются и добавляются барьерами загрузки и сохранения.
Тестирование
В реализации ZGC для неокрашенных и окрашенных указателей используются разные типы C++, и неявное преобразование между этими двумя типами невозможно. Окрашенные указатели используются только в коде сборщика мусора и в барьерах. Пока среда выполнения обращается к указателям на объекты через API доступа HotSpot и барьеры, она всегда будет видеть только неокрашенные указатели, которые можно разыменовать. Тип указателя на объект, видимый среде выполнения, всегда будет содержать неокрашенные указатели. Мы встраиваем в разные типы указателей на объекты обширный проверочный код, чтобы быстро обнаруживать повреждённые указатели или отсутствующие барьеры.
- Корректность будет подтверждена стандартным набором тестов для алгоритмов сборки мусора.
Риски и допущения
Сложность реализации
Барьеры и окрашенные указатели в Generational ZGC сложнее, чем в ZGC без поколений. Кроме того, в Generational ZGC одновременно работают два сборщика мусора. Эти сборщики в значительной степени независимы, но в некоторых случаях взаимодействуют довольно запутанным образом, и это увеличивает сложность реализации.
С учётом дополнительной сложности в долгосрочной перспективе мы намерены свести к минимуму затраты на сопровождение, полностью заменив исходную версию ZGC без поколений на Generational ZGC.
Производительность Generational ZGC будет отличаться от производительности ZGC без поколений
Мы считаем, что для большинства сценариев Generational ZGC подойдёт лучше своего предшественника. На некоторых нагрузках Generational ZGC может даже дать прирост пропускной способности за счёт меньшего потребления ресурсов. Например, в бенчмарке Apache Cassandra Generational ZGC требуется куча в четыре раза меньшего размера, но при этом пропускная способность в четыре раза выше, чем у ZGC без поколений, а время паузы по-прежнему остаётся меньше одной миллисекунды.
Некоторые нагрузки по своей природе не подходят для разделения на поколения, и на них производительность может немного снизиться. Мы считаем, что таких нагрузок достаточно мало и они не оправдывают затрат на долгосрочное сопровождение двух отдельных версий ZGC.
Один из дополнительных источников накладных расходов — более функциональные барьеры сборщика мусора. Мы ожидаем, что большую часть этих расходов компенсирует выигрыш от того, что объекты старого поколения не нужно часто собирать.
Ещё один дополнительный источник накладных расходов — одновременная работа двух сборщиков мусора. Нам нужно сбалансировать частоту их запуска и потребление ими процессора, чтобы они не оказывали чрезмерного влияния на приложение.
Как это обычно бывает при разработке сборщиков мусора, дальнейшие улучшения и оптимизации будут определяться бенчмарками и отзывами пользователей. Мы намерены продолжать улучшать Generational ZGC и после первого выпуска.