JEP 318: Epsilon: A No-Op Garbage Collector (Experimental)
Epsilon: сборщик мусора, не выполняющий никакой работы (Experimental (экспериментальная функция))
| Ответственный | Aleksey Shipilev |
| Тип | Feature |
| Область | Implementation |
| Статус | Closed / Delivered |
| Выпуск | 11 |
| Компонент | hotspot / gc |
| Обсуждение | hotspot dash gc dash dev at openjdk dot java dot net |
| Трудоёмкость | S |
| Длительность | S |
| Связан с | JEP 304: Garbage Collector Interface |
| Рецензенты | Andrew Haley, Roman Kennke |
| Одобрен | Mikael Vidstedt |
| Создан | 2017/02/14 08:23 |
| Обновлён | 2018/09/24 15:53 |
| Задача | 8174901 |
Аннотация
Разработать GC, который выделяет память, но не реализует никакого механизма её освобождения. Когда доступная куча Java будет исчерпана, JVM завершит работу.
Цели
Предоставить полностью пассивную реализацию GC с ограниченным лимитом выделения памяти и минимально возможными накладными расходами на задержку за счёт объёма занимаемой памяти и пропускной способности памяти. Успешная реализация — это изолированное изменение кода, которое не затрагивает другие GC и вносит минимальные изменения в остальную часть JVM.
Что не является целью
Целью не является добавление в язык Java и/или JVM средств ручного управления памятью. Целью не является добавление новых API для управления кучей Java. Целью не является изменение или чистка внутренних интерфейсов JVM под этот GC.
Мотивация
Реализации Java хорошо известны широким выбором гибко настраиваемых реализаций GC. Разнообразие доступных сборщиков в конечном счёте отвечает разным потребностям, даже если из-за возможностей настройки их функциональность пересекается. Иногда проще поддерживать отдельную реализацию, чем добавлять ещё один параметр конфигурации к существующей реализации GC.
Есть несколько сценариев, в которых тривиальный GC, не выполняющий никакой работы, оказывается полезен:
-
Тестирование производительности. GC, который почти ничего не делает, — полезный инструмент для сравнительного анализа производительности других, настоящих GC. GC, не выполняющий никакой работы, помогает отфильтровать артефакты производительности, вызванные GC: планирование рабочих потоков GC, стоимость барьеров GC, циклы GC, запущенные в неудачный момент, изменения локальности и т. д. Кроме того, есть артефакты задержки, не вызванные GC (например, сбои планирования, сбои при переходах между компиляторами и т. д.), и устранение артефактов, вызванных GC, помогает их выделить. Например, с GC, не выполняющим никакой работы, можно оценить естественный «фоновый» базовый уровень задержки для работы над GC с низкой задержкой.
-
Тестирование нагрузки на память. При тестировании кода на Java полезно иметь способ задать порог выделенной памяти, чтобы проверять инварианты нагрузки на память. Сегодня нам приходится получать данные о выделении памяти из MXBeans или даже прибегать к разбору журналов GC. GC, который допускает только ограниченный объём выделений и завершается с ошибкой при исчерпании кучи, упрощает тестирование. Например, зная, что тест должен выделять не более 1 ГБ памяти, мы можем настроить GC, не выполняющий никакой работы, с -Xmx1g и позволить ему аварийно завершиться с дампом кучи, если это ограничение нарушено.
-
Тестирование интерфейса VM. Для целей разработки VM простой GC помогает понять абсолютный минимум, который требуется от интерфейса VM-GC для работоспособного механизма выделения памяти. Для GC, не выполняющего никакой работы, в интерфейсе не должно быть ничего реализовано, а хороший интерфейс означает, что BarrierSet в Epsilon просто использует пустые реализации барьеров из реализации по умолчанию. Это служит доказательством того, что интерфейс VM-GC продуман разумно, что важно в свете JEP 304 («Garbage Collector Interface»).
-
Крайне кратковременные задачи. Кратковременная задача может рассчитывать на быстрое завершение, чтобы освободить ресурсы (например, память кучи). В этом случае допускать цикл GC, бесполезно очищающий кучу, — пустая трата времени, потому что куча всё равно будет освобождена при завершении. Заметим, что цикл GC может занять много времени, потому что его длительность зависит от объёма живых данных в куче, а их может быть много.
-
Улучшения задержки до последней капли. Для приложений, сверхчувствительных к задержке, разработчики которых внимательно следят за выделением памяти и точно знают объём памяти, занимаемый приложением, или даже для (почти) полностью не создающих мусора приложений допустить цикл GC может быть проблемой проектирования. Бывают также случаи, когда перезапуск JVM — с тем, чтобы балансировщики нагрузки обработали переключение на резерв, — иногда оказывается лучшей стратегией восстановления, чем допустить цикл GC. В таких приложениях долгий цикл GC может считаться неправильным решением, потому что он затягивает обнаружение сбоя и в конечном счёте задерживает восстановление.
-
Улучшения пропускной способности до последней капли. Даже для нагрузок, не выделяющих память, выбор GC означает выбор набора барьеров GC, которые нагрузка вынуждена использовать, даже если ни одного цикла GC на самом деле не происходит. Все GC в OpenJDK используют поколения (за заметными исключениями в виде Shenandoah и ZGC, не входящих в основную ветку), и они генерируют как минимум один барьер записи ссылок. Отказ от этого барьера может дать последний кусочек прироста пропускной способности. Здесь есть оговорки, связанные с локальностью, см. ниже.
Описание
Epsilon GC выглядит и ведёт себя как любой другой GC в OpenJDK и включается флагом -XX:+UseEpsilonGC.
Epsilon GC реализует линейное выделение памяти в одном непрерывном блоке выделенной памяти. Благодаря этому код выдачи TLAB (thread-local allocation buffer) в GC может быть тривиальным и не использовать блокировок, а затем переиспользовать уже существующий в VM код выделения памяти внутри TLAB без блокировок. Выдача TLAB также помогает ограничивать резидентную память процесса тем объёмом, который действительно был выделен. Выделение огромных объектов и выделение вне TLAB обрабатываются тем же кодом, потому что в этой схеме выделение TLAB мало отличается от выделения больших объектов.
Набор барьеров, используемый Epsilon, полностью пуст и ничего не делает, потому что GC не выполняет никаких циклов GC и, следовательно, его не интересуют граф объектов, маркировка объектов, копирование объектов и т. д. Добавление новой реализации набора барьеров, вероятно, будет самым существенным изменением JVM в этой реализации.
Поскольку единственная важная часть интерфейса среды выполнения для Epsilon — это выдача TLAB, его задержка во многом зависит от размеров выдаваемых TLAB. При сколь угодно больших TLAB и сколь угодно большой куче накладные расходы на задержку можно описать сколь угодно малым положительным значением, отсюда и название. (Альтернативная версия происхождения: «epsilon» часто означает «пустой символ», что соответствует природе этого GC, не выполняющего никакой работы.)
Когда куча Java исчерпана, никакое выделение памяти невозможно, никакое освобождение памяти невозможно, и поэтому мы вынуждены завершиться с ошибкой. На этом этапе есть несколько вариантов; большинство из них соответствуют тому, что делают существующие GC:
- Выбросить
OutOfMemoryErrorс понятным сообщением. - Выполнить дамп кучи (включается, как обычно, флагом
-XX:+HeapDumpOnOutOfMemoryError) - Жёстко завершить JVM и, при необходимости, выполнить внешнее действие (с помощью обычного
-XX:OnOutOfMemoryError=...), например запустить отладчик или уведомить внешнюю систему мониторинга о сбое.
При вызове System.gc() ничего делать не нужно, потому что код освобождения памяти не реализован. Реализация может предупреждать пользователей о том, что попытка принудительно запустить GC была бесполезной.
Пробные запуски прототипа подтверждают концепцию: он выдерживает небольшие нагрузки и предсказуемо завершается с ошибкой на более крупных. Реализацию прототипа и некоторые тесты можно найти в репозитории sandbox:
$ hg clone http://hg.openjdk.java.net/jdk/sandbox sandbox
$ hg up -r epsilon-gc-branch
$ sh ./configure
$ make images
Разницу между базовой и изменённой средой выполнения можно увидеть с помощью:
$ hg diff -r default:epsilon-gc-branch
Автоматически сгенерированный webrev: https://builds.shipilev.net/patch-openjdk-epsilon-jdk/
Примеры бинарных сборок: https://builds.shipilev.net/openjdk-epsilon-jdk/
Или в Docker:
$ docker pull shipilev/openjdk-epsilon
$ docker run -it --rm shipilev/openjdk-epsilon java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xlog:gc -version
[0.006s][info][gc] Initialized with 2009M heap, resizeable to up to 30718M heap with 128M steps
[0.006s][info][gc] Using TLAB allocation; min: 2K, max: 4096K
[0.006s][info][gc] Using Epsilon GC
openjdk version "11-internal" 2018-03-20
OpenJDK Runtime Environment (build 11-internal+0-nightly-sobornost-builds.shipilev.net-epsilon-jdkX-b32)
OpenJDK 64-Bit Server VM (build 11-internal+0-nightly-sobornost-builds.shipilev.net-epsilon-jdkX-b32, mixed mode)
[0.071s][info][gc] Total allocated: 899 KB
[0.071s][info][gc] Average allocation rate: 12600 KB/sec
Альтернативы
Настроить существующие GC так, чтобы они никогда не выполняли цикл. Например, сборщики Serial или Parallel должны укладываться в тот же профиль задержки, если мы сможем настроить их эвристики так, чтобы они никогда не выполняли циклы GC до полного исчерпания кучи (то есть заранее задав размер кучи, установив очень большой размер молодого поколения, отключив адаптивные эвристики и т. д.). Надёжно гарантировать это трудно из-за множества параметров GC, которые они предоставляют, и из-за продолжающихся улучшений GC, которые заставят нас дважды подумать о путях без выполнения работы.
Изменить существующие GC так, чтобы они никогда не выполняли цикл. Мы можем добавить в эти GC специальные параметры, чтобы сделать это надёжнее, но это может противоречить целям проектирования этих GC. Например, защита большинства путей выполнения кода этих GC с помощью DoNotGC выглядит ненамного лучше, чем отдельная самостоятельная реализация.
Выпотрошить существующую реализацию GC. Альтернативой было бы заменить пустыми операциями существующую реализацию GC, чтобы получить базовую реализацию для тестирования. Проблема здесь в неудобстве: разработчикам пришлось бы убеждаться, что такая реализация по-прежнему корректна, что её производительности достаточно для хорошей базовой линии, что она подключена к другим средствам среды выполнения (дамп кучи, обход стеков потоков, MXBeans) для дополнения сравнительного анализа. Реализации для других платформ потребовали бы гораздо больше работы. Готовая к использованию реализация, не выполняющая никакой работы, в основной ветке устраняет это неудобство.
Выпотрошить существующий набор барьеров GC. Существующих вариантов, которые отключают все барьеры GC, нет, но можно заменить заглушками набор барьеров существующего GC. К сожалению, это порождает те же проблемы, что и выше, и к ним добавляется острая необходимость отключить GC после такого потрошения, потому что базовые инварианты, которые GC ожидает от барьеров, перестанут выполняться.
Дальнейшие улучшения в сборщиках Parallel, G1 и Shenandoah со временем могут снизить накладные расходы настолько, что GC, не выполняющий никакой работы, больше не понадобится. Если и когда это произойдёт, Epsilon всё равно будет полезен для тестирования нагрузки на память и производительности.
Тестирование
Обычные тесты GC не подойдут для Epsilon GC, потому что большинство тестов предполагает, что они могут выделять произвольный объём мусора. Потребуется разработать новые тесты, чтобы проверить, что GC действительно хорошо работает на нагрузках с малым объёмом выделения памяти и что он предсказуемо завершается с ошибкой при исчерпании кучи. Для проверки корректности будет достаточно новых тестов jtreg в hotspot/gc/epsilon.
Разового тестирования производительности во время разработки будет достаточно, чтобы обеспечить нужные характеристики производительности при работе с интерпретатором и компиляторами C1 и C2. Постоянное тестирование производительности не требуется, поскольку реализация не должна меняться после первоначальной реализации, а её чувствительные к производительности пути неявно тестируются другими GC.
Риски и допущения
Полезность против затрат на сопровождение. Можно возразить, что такая реализация бесполезна в продукте, потому что она никому не нужна. Однако опыт показывает, что многие участники экосистемы Java уже проделали это упражнение, удалив GC из своих собственных сборок JVM. Это значит, что стандартный вариант GC, не выполняющего никакой работы, помог бы этой части экосистемы. С учётом низких затрат на сопровождение, если реализация окажется тривиальной, этот риск минимален. Мы также считаем, что этот риск минимален, если возможность останется доступной только в непродуктовых сборках, под флагом «develop». Пользователи и производные дистрибутивы могут изменить его на «product» или «experimental», чтобы сделать Epsilon доступным для своих приложений.
Ожидания публики. Предоставление сборщика мусора, который на самом деле не выполняет сборку мусора, может рассматриваться как опасная практика. Случайное включение Epsilon GC в продакшене может привести к неожиданным сбоям JVM при исчерпании кучи. Мы считаем, что этот риск минимален, если возможность останется по умолчанию недоступной в продуктовых сборках, под параметром «develop» или «experimental».
Соображения локальности. Неуплотняющий GC неявно означает, что он сохраняет граф объектов в порядке их выделения. Это влияет на пространственную локальность, и обычные приложения могут столкнуться со снижением пропускной способности, если выделения памяти случайны или порождают много разрежённого мусора. Хотя это может повлечь некоторые накладные расходы по пропускной способности, это вне контроля GC и затронет большинство неперемещающих GC. Если локальность окажется проблемой, для смягчения этого недостатка потребуется писать код приложения с учётом локальности.
Сложность реализации. Может оказаться, что реализации потребуется больше изменений в общем коде, чем ожидалось, например в компиляторах и платформенно-зависимых бэкендах. Наш прототип показывает, что эти изменения достаточно изолированы и потому безвредны. Если это окажется риском, его должен смягчить JEP 304 («Garbage Collector Interface»).
Зависимости
Эта работа может зависеть от JEP 304 («Garbage Collector Interface»), чтобы свести к минимуму изменения общего кода. Однако этот интерфейс может и не потребоваться, если изменения общего кода будут минимальными.