Дюк, вынеси мусор! — 5. Epsilon GC +20


Сборщики мусора из OpenJDK, которые мы успели рассмотреть к этому моменту (Serial и Parallel, CMS и G1, ZGC), были нацелены на как можно более быструю и эффективную сборку мусора, для чего использовали техники различной степени сложности и изобретательности. Это вполне ожидаемо, ведь исходя из названия, борьба с мусором — это их основная обязанность.

Но сегодня у нас на рассмотрении сборщик, который выбивается из общей картины. Его разбор будет недолгим, но полезным, так как позволит взглянуть на один не рассматривавшийся до этого аспект работы сборщиков. Давайте немного отдохнем от сложных технических трюков и разберемся с Epsilon GC — самым простым из входящих в состав OpenJDK сборщиков.


Подход, используемый Epsilon GC, описывается коротко — он вообще не собирает мусор, а просто завершает работу приложения сразу, как только оно попыталось аллоцировать больше памяти, чем ему дозволено (больше значения Xmx).

Как можно понять из этого описания, реализовать такой сборщик не очень сложно. Его стабильная версия была добавлена еще в JDK 11, но при этом он до сих пор формально имеет статус экспериментального. Этот статус было решено оставить за ним навечно, чтобы для его включения требовалось указывать дополнительную опцию, которая лишний раз привлекает внимание и напоминает о том, что приложение использует что-то необычное.

Поэтому для включения Epsilon GC необходимо указать сразу две опции: -XX:+UnlockExperimentalVMOptions и -XX:+UseEpsilonGC.

Принципы работы

Сборки мусора у Epsilon GC нет, но это не значит, что о принципах его работы совсем уж нечего написать, ведь сборщики влияют не только на то, как удаляется мусор, но и на то, как аллоцируется память под новые объекты. До сих пор при рассмотрении сборщиков мусора мы лишь поверхностно касались этого вопроса. Давайте воспользуемся случаем и посмотрим на него чуть более внимательно.

В JVM для размещения новых объектов используются TLAB'ы (thread-local allocation buffers), то есть относительно небольшие буферы памяти, которые отдельные потоки запрашивают в куче, а затем используют для размещения в них объектов, создаваемых в этих потоках, не конкурируя ни с кем за доступ к куче, пока не понадобится новый буфер. Такой подход позволяет значительно ускорить процесс создания новых объектов. Для так называемых громадных объектов (humongous objects), не помещающихся в буфер, в куче запрашиваются блоки памяти специально под них.

Когда какому-либо потоку не удается получить очередной буфер из кучи по причине исчерпания ее ресурсов, другие сборщики могут запустить цикл сборки и попробовать высвободить недостающее место, а Epsilon GC просто генерирует OutOfMemoryError и завершает процесс.

Настройка

Так как из обязанностей у Epslion GC остались только запросы TLAB'ов нужного размера, почти все настройки крутятся вокруг них:

Опции -XX:+EpsilonElasticTLAB и -XX:EpsilonTLABElasticity=elasticity позволяют управлять режимом эластичности TLAB'ов, то есть динамически изменять их размеры отдельно для каждого потока в зависимости от его аппетитов к памяти.

Опции -XX:+EpsilonElasticTLABDecay и -XX:EpsilonTLABDecayTime=decay_time (в мсек) развивают идею эластичности, позволяя периодически сбрасывать размер TLAB'ов к исходному, чтобы, например, период старта приложения с активным выделением памяти не мог сильно влиять на последующие аллокации.

С помощью опции -XX:EpsilonMaxTLABSize=size можно ограничивать размер TLAB'ов сверху.

Опция -XX:EpsilonMinHeapExpand=size задает минимальный размер, на который JVM увеличивает размер кучи при необходимости ее расширения.

Достоинства и недостатки

На первый взгляд идея такого сборщика может показаться странной, но у него есть свои сценарии использования, ведь отказ от сборки мусора означает и отказ от больших накладных расходов на тщательный учет объектов, на их разделение по регионам, на дополнительные потоки сборщика, на барьеры при доступе к объектам и на прочие механизмы, которые используются другими сборщиками.

Но следует иметь в виду и обратную сторону этой медали. Тот факт, что мусор не собирается, не означает, что его нет. Он обычно есть и приводит к дефрагментации памяти, занятой живыми объектами, что потенциально может сказываться на скорости доступа к ним.

Так когда же плюсы Epsilon GC перевешивают тот факт, что ваша программа не будет пытаться собирать мусор, а просто завершится как только создаст достаточно большое количество объектов?

Во-первых, он может использоваться приложениями, которые при старте создают все нужные им объекты, а после этого вообще не мусорят. В этом случае и первоначальное выделение памяти и дальнейшая работа может происходить быстрее.

Во-вторых, он подойдет короткоживущим приложениям, которые сами по себе завершаются быстрее, чем успевают занять весь разрешенный им объем памяти. Например, какой-нибудь консольной утилите, для которой вы точно можете определить максимальный объем памяти с учетом всего генерируемого мусора.

В-третьих, его можно использовать для целей анализа накладных расходов, привносимых другими сборщиками конкретно в ваше приложение. Просто запускаете свою программу с Epsilon GC и пока она работает собираете метрики производительности и используемых ресурсов. А потом сравниваете их с аналогичными метриками при использовании своего целевого сборщика, получая примерное представление о том, во что он вам обходится. Такой сценарий использования подходит в том числе самим разработчикам JVM и других сборщиков мусора.

То есть Epsilon GC не такой уж и бесполезный, своя ниша у него есть и о его существовании стоит помнить.

Часть 6 — Сборщик Shenandoah GC →

Ранее:

← Часть 4 — Сборщик ZGC

← Часть 3 — Сборщики CMS и G1

← Часть 2 — Сборщики Serial GC и Parallel GC

← Часть 1 — Введение




Комментарии (6):

  1. mortadella372
    /#24603626 / -2

    Лучший GC для кубернетес-приложений?

    • alygin
      /#24603846

      Выбор сборщика в большей степени зависит от специфики самого приложения (от требований к нему и от того, как оно работает с памятью), нежели от того, в какой инфраструктуре вы его запускаете. Нужно только учитывать ограничения на доступные JVM ресурсы, которые накладывает инфраструктура, а в остальном смотрите на само приложение, однозначного ответа здесь нет.

      Нужно только помнить про особенности работы ZGC, который из-за использования цветных указателей (см. предыдущую статью) может репортить трехкратное значение используемой памяти, что может ставить в тупик какие-нибудь системы контейнеризации или системы их мониторинга. Да и просто людей может смущать.

      • mortadella372
        /#24603920

        Ну, просто если оркестратор имеет свой контроль за памятью, то зачем дублировать эту логику на уровне, фактически, приложения (пода)?

        • alygin
          /#24603938

          Оркестратор и JVM управляют памятью на разных уровнях. Оркестратор может выделить контейнеру с Java-процессом нужный объем памяти, но он никак не вмешивается в ее дальнейшее использование, он ничего не знает про Java-объекты, кучу, сборку мусора и т. п., это не его зона ответственности. Это уже задача JVM использовать выделеннную ей память для организации в рамках нее всего перечисленного.

          • mortadella372
            /#24604016

            Да, разумеется. Но не делает ли оркестратор всю систему менеджмента памяти на уровне приложения некоторым образом устаревшей? Ну, вторичной по крайней мере.

            Логика примерно такая. Если приложение/сервис может жить под оркестратором, значит наши бизнес-требования могут мириться с периодическими рестартами отдельных подов. За счет ли резервирования с балансировкой, или еще почему. А раз так, то зачем нам заниматься настройкой управления памяти дважды? Выставить в Epslion GC значение побольше, и забыть про него, положившись на оркестрацию.

            Разменяв, таким образом, потери производительности от GC на потери от перезапуска пода.

            • alygin
              /#24604084

              Технически, конечно, это будет работать. Но обычно даже когда разрабатывают сервисы, которые могут мириться с периодическими рестартами отдельных подов, не рассчитвают на то, что рестарты будут настолько частыми. Если вы изначально не разрабатываете приложение с таким расчетом, чтобы оно очень мало мусорило (а это очень сложно), то падать по OOM с Epsilon'ом оно будет очень часто.

              Либо вам придется выделать ему память с очень большим запасом, что намного дороже, чем подобрать сборщик, разменивающий относительно небольшой перерасход памяти и процессора на эффективную сборку, и жить с ним.

              То есть за крайне редкими исключениями, я думаю, описанный вами подход будет заметно более дорогим, чем при использовании другого сборщика.