Дюк, вынеси мусор! — 5. Epsilon GC
Сборщики мусора из 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 и других сборщиков мусора.
То есть сборщик не такой уж и бесполезный, своя ниша у него есть и о его существовании стоит помнить.
Ранее:
← Часть 4 — Сборщик ZGC
← Часть 3 — Сборщики CMS и G1
← Часть 2 — Сборщики Serial GC и Parallel GC
← Часть 1 — Введение