JEP 423: Region Pinning for G1
Закрепление регионов в G1
| Автор | Hamlin Li |
| Ответственный | Thomas Schatzl |
| Тип | Feature |
| Область | Implementation |
| Статус | Closed / Delivered |
| Выпуск | 22 |
| Компонент | hotspot / gc |
| Обсуждение | hotspot dash gc dash dev at openjdk dot java dot net |
| Трудоёмкость | L |
| Длительность | M |
| Рецензенты | Thomas Schatzl, Vladimir Kozlov |
| Одобрен | Vladimir Kozlov |
| Создан | 2021/10/28 08:05 |
| Обновлён | 2024/02/05 16:37 |
| Задача | 8276094 |
Аннотация
Снизить задержку, реализовав в G1 закрепление регионов, чтобы не требовалось отключать сборку мусора во время критических областей Java Native Interface (JNI).
Цели
-
Никаких остановок потоков из-за критических областей JNI.
-
Никакой дополнительной задержки перед запуском сборки мусора из-за критических областей JNI.
-
Никакого ухудшения времени пауз GC, когда активных критических областей JNI нет.
-
Минимальное ухудшение времени пауз GC при активных критических областях JNI.
Мотивация
Для взаимодействия с неуправляемыми языками программирования, такими как C и C++, JNI определяет функции для получения и последующего освобождения прямых указателей на Java-объекты. Эти функции всегда должны использоваться парами: сначала нужно получить указатель на объект (например, через GetPrimitiveArrayCritical), а затем, после работы с объектом, освободить указатель (например, через ReleasePrimitiveArrayCritical). Считается, что код между такими парами функций выполняется в критической области, а Java-объект, доступный для использования в это время, называется критическим объектом.
Когда Java-поток находится в критической области, JVM должна следить за тем, чтобы не перемещать связанный с ней критический объект во время сборки мусора. Для этого она может закреплять такие объекты на их местах, по сути фиксируя их, пока GC перемещает другие объекты. Либо она может просто отключать GC всякий раз, когда поток находится в критической области.
Сборщик мусора по умолчанию, G1, использует второй подход и отключает GC на время каждой критической области. Это существенно влияет на задержку: если Java-поток инициирует GC, он должен ждать, пока ни один другой поток не будет находиться в критической области. Тяжесть последствий зависит от частоты и длительности критических областей. В худших случаях пользователи сообщают о том, что критические секции блокируют всё приложение на минуты, о лишних ситуациях нехватки памяти из-за голодания потоков и даже о преждевременном завершении VM. Из-за этих проблем сопровождающие некоторых Java-библиотек и фреймворков решили не использовать критические области по умолчанию (например, JavaCPP) или не использовать их вовсе (например, Netty), хотя это может ухудшить пропускную способность.
С предлагаемым здесь изменением Java-потоки никогда не будут ждать завершения операции G1 GC.
Описание
Предыстория
G1 делит кучу на регионы памяти фиксированного размера (не путать с критическими областями). G1 — сборщик мусора с поколениями, поэтому любой непустой регион принадлежит либо молодому, либо старому поколению. В каждой отдельной операции сборки объекты эвакуируются (то есть перемещаются) только из части регионов в некоторую другую часть регионов.
Если во время малой сборки (то есть сборки молодого поколения) G1 не может найти место для эвакуации объекта, он оставляет объект на месте и помечает и сам объект, и содержащий его регион как не прошедшие эвакуацию. После эвакуации G1 обрабатывает такие регионы, переводя их из молодого поколения в старое и, возможно, оставляя их готовыми к последующей эвакуации.
G1 уже умеет закреплять объекты на их местах в памяти во время больших (то есть полных) сборок — просто не эвакуируя регионы, в которых они находятся. Например, G1 закрепляет humongous-регионы, содержащие большие объекты. Кроме того, на время одной сборки он закрепляет любой регион, превышающий заданный порог доли живых объектов.
Во время малых сборок G1 не может закреплять произвольные регионы, хотя humongous-регионы из таких сборок он исключает.
Закрепление регионов во время малых сборок
Мы намерены достичь перечисленных выше целей, доработав G1 так, чтобы он мог закреплять произвольные регионы во время как больших, так и малых сборок. Это делается следующим образом:
-
Для каждого региона вести счётчик критических объектов в нём: увеличивать его, когда получен критический объект из этого региона, и уменьшать, когда этот объект освобождён. Если счётчик равен нулю, выполнять сборку мусора в регионе как обычно; если счётчик не равен нулю, считать регион закреплённым.
-
Во время большой сборки не эвакуировать закреплённые регионы.
-
Во время малой сборки считать закреплённые регионы молодого поколения не прошедшими эвакуацию и тем самым переводить их в старое поколение. Уже закреплённые регионы старого поколения не эвакуировать.
После этого мы сможем реализовать критические области JNI без отключения GC: регионы, содержащие критические объекты, будут закрепляться, а сборка мусора в незакреплённых регионах будет продолжаться.
Альтернативы
Спецификация JNI предлагает ещё два способа реализации критических областей:
-
В начале критической области копировать критический объект в кучу C, где он не будет перемещаться; в конце критической области копировать его обратно.
Это очень неэффективно и по времени, и по памяти. В G1 можно было бы делать так только для критических объектов в регионах, которые нельзя закрепить. Однако такие регионы находятся в молодом поколении, где обычно и происходит бо́льшая часть использования и изменения объектов, поэтому мы не ожидаем, что это сильно помогло бы.
-
Закреплять объекты по отдельности.
G1 может эвакуировать только регионы целиком, поэтому единственный закреплённый объект в регионе не позволил бы собрать этот регион. Итог мало отличался бы от предложенного выше, но накладные расходы были бы выше, поскольку отслеживать отдельные закреплённые объекты дороже, чем вести для каждого региона счётчик критических объектов.
Тестирование
Помимо функциональных тестов, мы проведём бенчмарки и измерения производительности, чтобы собрать данные о производительности, необходимые для подтверждения того, что наши цели достигнуты.
Риски и допущения
Мы предполагаем, что ожидаемый характер использования критических областей JNI не изменится: они по-прежнему будут использоваться редко и будут кратковременными.
Существует риск исчерпания кучи, если приложение одновременно закрепит много регионов. Решения этой проблемы у нас нет, но то, что Shenandoah GC закрепляет регионы памяти во время критических областей JNI и не сталкивается с этой проблемой, позволяет предположить, что для G1 она тоже не станет проблемой.