JVM
Ниже будут рассматриваться принципиальные особенности окружения, необходимые для профессиональной разработки на Java.
HotSpot JVM
Основная JVM, поставляемая Oracle (а когда-то Sun). Поскольку владельцем торговой марки Java сегодня является Oracle, а также, подавляющее большинство систем работают именно на этой JVM, большинство особенностей работы JVM будут рассматриваться именно на имплементации виртуальной машины.
Память

Область памяти на рабочей станции, которая выделяется ОС для процесса JVM и ей же управляется. Глобально делится на 3 области:
- Java Heap - содержит все объекты всех классов, а так же все массивы (
T[]); - Off-Heap
- Metaspace, ранее PermGen - содержит информацию по классам, описание их структуры, константы (
static), аннотации на объекты и их объявление, и прочее. Для управления используется-XX:MetaspaceSize, который позволяет избежать слишком быстрого первичного запуска ; - CodeCache - содержит динамически сгенерированный код (например, Just-In-Time, JIT C1, C2 и GraalVM) и интерпретаторы. Используется JIT для хранения скомпилированного нативного кода. В случае недостатка свободного места в этой области памяти, JIT начнет обрабатывать байт-код на лету, что замедлит выполнение. Регулируется
-XX:ReservedCodeCacheSize,-XX:InitialCodeCacheSizeи-XX:+UseCodeCacheFlushing(высвобождает память от старых методов) - Direct Memory - используется для буферизации (прямой буферизации). В частности, используется NIO для буферизации данных на вход/выход. В случае переполнения буферов будет возникать
OutOfMemoryError. Регулируется-XX:MaxDirectMemorySize
- Metaspace, ранее PermGen - содержит информацию по классам, описание их структуры, константы (
- Thread Stacks - содержит фрэймы выполнения для интерпретируемого, скомпилированного и нативного стэков
- C-Heap - содержит байт-код самой JVM, объекты MMAP, нативные объекты и прочие служебные данные. Наиболее важно для разработчика - в данной области хранятся данные потоков (
Thread) - только примитивов. Для ее мониторинга можно использовать только средства ОС, в частности,pmap,svmonи подобные. Для 32-bit систем, данная область ограничена по формуле:4GB - Java Heap - PermGen; для 64-bit:Total RAM - Java Heap - PermGen.
Java Heap
Может быть увеличена или уменьшена посредством аргумента при запуске JVM -Xmx и -Xms (макс. и мин, соответственно). По умолчанию, устанавливается 25% максимального в принципе доступного для JVM объема памяти (узнать его можно выполнив java -XX:+PrintFlagsFinal и найдя в перечне флагов JVM MaxHeapSize, 25% от него и будет размером Heap по умолчанию).
Для мониторинга могут быть использованы: трассировка команд GC (verbose GC), JMX API, JConsole, и прочее.
Разделена на 3 участка: Young Generation, Old Generation и Permanent Generation.

Permanent Generation
Уникальное хранилище, используемое только HotSpot JVM. Хранит мета-информацию по классам (наименования, методы и их сигнатуры, взаимозависимости классов), внутренние JVM объекты и данные, необходимые для Just-In-Time, JIT оптимизации. Для мониторинга данной области используются те же инструменты: трассировка команд GC (verbose GC), JMX API, JConsole, и прочее.
Young Generation
Используется для создания новых объектов. При заполнении данной области, объекты из нее перемещаются в Old Generation, высвобождая область для новых объектов. Важно отметить, что на момент переноса и чистки, область становится недоступной, и, JVM блокирует все операции по созданию объектов (можно сказать, останавливает работу JVM). Поскольку данный процесс максимально оптимизирован и имеет приоритет для CPU, он выполняется быстро и почти незаметно.
Данная область так же делится на несколько различных частей:
- Eden. В этой области создаются новые объекты - там они находятся начиная с первой инструкции по созданию объекта вплоть до полного заполнения данной области (весьма небольшой). Именно в момент чистки этой области, выполняется полная блокировка на создание(!) новых объектов - процесс выполняется Garbage Collector, именуемый как Minor CG;
- Survivor Spaces, S0 и S1. В эти области объекты попадают из Eden и из соседней Survivor после того, как область Eden заполнена и в отношении нее был выполнен Minor GC. Принцип заполнения довольно прост - из одной области, которая имеет объекты (допустим, это будет S0 (которая будет выступать в роли Survivor From), активные объекты перемещаются в область S1 (которая будет выступать в роли Survivor To) в дополнение к некоторым (или всем) объектам из Eden. Обе эти области (S0 и S1) имеют одинаковый размер. Очень похожим способом, добавляя в процесс воду, используют два сита с разным диаметром ячейки, выполняют просеивание песка. Объекты в данной области также буду находиться там не вечно - они будут перемещены в область Old Generation когда возникнет такая необходимость.
Размер зависит Young Generation зависит от конфигурации, и по умолчанию составляет 33.3% от всей Java Heap. Но размер может управляться с помощью параметра при старте JVM: XX:NewRatio=1
Old Generation
В данной области расположены долгоживущие объекты. При ее заполнении выполняется сборка мусора Major CG, которая не замораживает работу JVM (в отличие от Minor GC). Впрочем, данная область чистится значительно реже и ее заполнение намного более плавное и растянутое по времени, поскольку: (1) не все объекты доживают до данной области; (2) зачастую, область Old больше чем Young Generation в два и более раза.
Из Old Generation объекты удаляются только после того, как Major GC их удалит навсегда. Признаком для их удаления является то, что на объект нет никакой внешней ссылки. Цикличные ссылки - два объекта имеют ссылку лишь друг на друга - также являются претендентами на удаление.
Зачем все усложнять?
Вся процессия с перетеканием объектов Eden -> S0 -> S1 -> S0 служит одной цели - для нивелирование фрагментации RAM памяти. ОС не может выделить один большой, последовательный объем памяти и, как следствие, не отказывает в выделении вовсе, что влечет за собой ошибку JVM.
C-Heap
Данная область редко рассматривается в качестве источника проблем ввиду того, что в ней не хранятся данные, создаваемые программой. Несмотря на это, данная область легко может создать проблемы для приложения, в особенности, для x32 систем. Это связано с тем, что при выделении большего объема Java Heap, JVM сужает C-Heap. И получается, что при видимом расширении объема доступной JVM памяти, одна из ее областей уменьшается, что может спровоцировать OutOfMemoryError.
Данная область памяти может переполниться еще и вот почему: как было указано выше, для разработчика эта область важна тем, что именно в ней хранятся данные каждого потока - при создании потока, создается отдельный экземпляр области памяти Stack, в которой хранится вся иерархия вызовов программных инструкций (Stack Frames), все примитивные локальные переменные, которые были созданы в рамках потока, результаты методов и указатели на области памяти в Java Heap тех объектов, которые были созданы в рамках потока. Также, в рамках разговора о потоках и области памяти C-Heap, стоит упомянуть о специальном счетчике PC Register, который указывает на текущую программную инструкцию, выполняемую каждым потоком.
Как JVM работает при старте
- Загружаются все необходимые нативные компоненты в C-Heap, де-факто, запускается стандартная программа, написанная на языке C;
- Используя интерпретатор, загружаются стандартные библиотеки Java, входящие в поставку JVM в область Native Heap;
- Загружаются JAR библиотеки, которые “видны” в Classpath;
- Запускается первый, начальный поток (Main Thread);
- Для каждого загруженного класса, все
staticобъекты сохраняются в PermGen область; - Выполняется
mainметод, в ходе которого данные распределяются по соответствующим областям в рамках Java Heap и Stack; - Периодические запуски GC выполняют удаление из памяти ненужных объектов;
Выявление проблем
- Сколько работающих приложений (jar, war, ear) запускаются на одной JVM? Увеличение потребления памяти не пропорционально количеству потреблению на разных JVM (несколько приложений на одной JVM несут дополнительные издержки).
- Количество загруженных классов увеличивает нагрузку на Metaspace и замедляет оптимизацию JIT.
- Передача больших объемов данных между внешними источниками и приложением (например, работа по сети, с базой данных или чтение файлов) раздувает буфер, который впоследствии попадет в Java Heap Old Generation. Передача может быть настолько быстрой и объемной, что он может просто не успевать очищаться
- Чрезмерное выделение потоков (
Thread) может спровоцироватьOutOfMemoryErrorв C-Heap - Для x32 систем не рекомендуется выделение более 2GB (
-Xms2048m,-Xmx2048m) памяти из-за переполнения C-Heap. Для x64 стоит указывать от 3GB - Желательно соблюдение баланса 1:3 (или 33%) Young Generation относительно Old Generation. Минимализация сборки мусора является ключом для повышения производительности.
- Для минимизации частоты сборки мусора (наиболее дорогой операции, выполняемой JVM за исключением бизнес-задач), желательно сбалансировать память таким образом, чтобы Java Heap после процедуры полной работы GC, оставался на уровне 50% от максимального порога.
- Рекомендуется разбивать большие приложения на более мелкие со своими микро-задачами. Это позволит, в том числе, переместить приложения с одной JVM на несколько независимых. Правда, стоит соблюдать баланс - цена за транспортировку данных может быть выше, чем горизонтальное масштабирование.
Сторонние JVM
Несмотря на то, что Java принадлежит Oracle, существуют и другие поставщики JVM, который, как правило, стараются отобрать у Oracle специфическую часть рынка. Некоторые JMV и их яркие особенности будут рассматриваться ниже.
IBM JVM
Поставляется компанией IBM вместе с собственной JVM - IBM J9, сегодня Eclipse OpenJ9.
Память
В данной имплементации выделяются только две области - Java Heap и Native Heap. Non-heap область, которая использовалась для хранения мета-данных классов, конструкторы и прочее, перенесены теперь в Native Heap.
Для мониторинга Java Heap используются те же инструменты, но мониторинг для специализированного GC, предоставляемого совместно с JVM - gencon. Подробнее о нем ниже.
GC
Для данной JVM характерно фокусирование на хранение данных в памяти с низкой частотой обновления - де-факто, с долгим хранением данные в памяти. Собственная реализация GC, gencon оптимизирована именно под эти требования. Включается параметром -Xgcpolicy:gencon, указывающимся при старте JVM.
JRockit
Младший брат HotSpot, также поставляемый компанией Oracle. Со временем, компания планирует объединение этих JVM в единый продукт, взяв положительные качества каждой из них.
Память
Имеет только две области памяти - Java Heap и Native Heap. В связи с тем, что JRockit JVM имеет возможность запускать скомпилированный байт-код, случаи возникновения OutOfMemoryError более вероятны, чем в случае HotSpot JVM. Это обусловлено тем, что JVM вынуждена загружать намного больше данных в Native Heap, провоцируя его кратное увеличение. Для x64 систем это не имеет такой остроты, поскольку x32 системы отдают предпочтение Java Heap, забирая память у Native Heap.
Corretto
Этот раздел в разработке.
GraalVM
Этот раздел в разработке.
JPDheap
JPDheap - модификация Java Heap, которая, по заявлениям разработчиков, увеличивает эффективность Heap для приложений, работающих с большими объемами.
Этот раздел в разработке.
Оптимизация
Перед описанием возможных инструментов оптимизации стоит отметить, что универсального сценария или идеального набора параметров не существует! Каждое приложение уникально и для каждого из них может быть применен свой набор параметров и инструментов.
Общая
UseCompressedOops
-XX:+UseCompressedOops boolean. Актуально только для x64 - сжимает ссылку на объекты с 8B до 4B. Если при старте Java Heap объявлен меньше 32GB, автоматически true. Оптимизация достигается снижением нагрузки на оперативную память и уменьшением количества промахов при обращении к кэшу (cache misses). Сборщик ZGC не разрешает включать эту опцию.
UseCompressedClassPointers
-XX:+UseCompressedClassPointers boolean указатель на структуру класса будет храниться как 32-битное смещение, а не как 64b адрес. Это снижает объем используемой памяти, но провоцирует нестабильность JVM. По этой причине, рекомендуется отказаться от его использования. Он, вообще, носит флаг deprecated.
UseLargePages
-XX:+UseLargePages boolean. Позволяет использовать большие блоки памяти (2MB - 1GB) вместо стандартных (4KB). Использование больших блоков памяти оптимизирует работу процессора с Translation-Lookaside Buffers, TLB. Эффект будет ощутим только при условии необходимости хранения больших и долгоживущих Java объектов, которые полноценно используют Java Heap.
Следует включать только если показатель DTLB_LOAD_MISSES демонстрирует высокие значения (этот показатель фиксирует количество неудачных поисков в физическом хранилище памяти относительно имеющегося виртуального адреса). Если показатель высокий, это говорит о том, что процессору часто приходится тратить полный обход блоков с фактичекскими адресами данных, вместо использования кэша. Существует несколько способов узнать этот показатель, например:
1 | perf stat -e dTLB-loads,dTLB-load-misses,dTLB-prefetch-misses ${YOUR_RUNNABLE_APPLICATION} |
Может потребовать открытия опции профилирования (потребует sudo прав):
1 | sudo sysctl kernel.perf_event_paranoid=-1 |
Анализ производится для любого запускаемого приложения. Ниже будет пример запуска анализа для примитивного Java приложения:
1 | package tech.dchirikov; |
1 | perf stat -e dTLB-loads,dTLB-load-misses,dTLB-prefetch-misses java tech.dchirikov.Main |
С выводом:
1 |
|
Уровень потерь (в примере выше, 0.11%) зависит от соответствующих требований.
Больше информации по профилированию приложений с помощью perf, а также, примеры использования.
UseNUMA
-XX:+UseNUMA (default false). По умолчанию false. Доступно только для серверов с процессорами, построенными на архитектуре NonUniform Memory Architecture, NUMA, а также требует одного из следующих GC: Parallel GC, G1, and ZGC.
Опция помогает распределять память в Java Heap по различным узлам максимально близко к тому процессору, к которому область памяти расположена максимально близко - так длина пути для доступа процессора к ячейке сокращается и появляется прирост в скорости.
LD_PRELOAD
LD_PRELOAD calloc_mem_allocation. По умолчанию, используется malloc, а тот, в свою очередь, mmap для поиска адреса ячейки в памяти. Может быть заменено одним из вариантов: jemalloc, tcmalloc, mimalloc. Но замена может повлечь OutOfMemoryError, или альтернативно, увеличение Java Heap. Но зачем столько проблем?
Java Heap
AlwaysPretouch
-XX:+AlwaysPreTouch заставляет JVM выделять чистую физическую память для всей Java Heap. В момент запуска, Java заставит выделить все необходимые блоки памяти и заставит их очиститься (установит в 0). Выделение памяти будет производиться ровно, без необходимости запрашивать память “на лету” и очищать ее (задерживая процессор все это время). Однако, есть и издержки - долгий старт. Данная опция рекомендована в случаях, когда нужно максимально сократить потенциальные задержки по ходу исполнения! В особенности, если ожидается, что сервис будет оперировать большими объемами данных.
Metaspace
InitialBootClassLoaderMetaspaceSize
-XX:InitialBootClassLoaderMetaspaceSize number. Определяет размер блока для стартующих классов. Используется для оптимизации потребления C-Heap или где сильные ограничения по RAM (например, в IoT).
MaxMetaspaceExpansion
-XX:MaxMetaspaceExpansion number. Определяет максимальное расширение Metaspace, которое доступно без необходимости полноценного запуска GC.
MinMetaspaceFreeRatio и MaxMetaspaceFreeRatio
-XX:MinMetaspaceFreeRatio number. По умолчанию, 40%. Определяет минимальный процент свободной памяти в Metaspace после работы GC. Если памяти будет недостаточно, GC будет более агрессивен при следующем запуске.
-XX:MaxMetaspaceFreeRatio number. По умолчанию, 70%. Ограничивает максимальную свободную память в Metaspace после работы GC.
Оба эти параметра образуют пару и ограничивают свободную память в Metaspace, помогая ограничить свободное пространство с обеих границ.
CodeCache
Как параметры для этой области не меняют, но некоторые можно упомянуть:
InitialCodeCacheSize
-XX:ReservedCodeCacheSize number - максимальный размер кэша для сгенерированного кода.
Direct Memory
MaxDirectMemorySize
-XX:MaxDirectMemorySize number. По умолчанию 0, что указывает на то, что JVM будет выделять буфер для I/O операций автоматически.
JVM
OmitStackTraceInFastThrow
-XX:+OmitStackTraceInFastThrow boolean. По умолчанию true. При true - для часто возникающих ошибок, типа NullPointerException, объекты с исключением не будут создаваться заново, а вместо этого будут переиспользоваться созданные заранее.
StackTraceInThrowable
-XX:+StackTraceInThrowable boolean. По умолчанию true. При true, в случае ошибки, будет записываться весь Stack Trace.
GCs
UseGCOverheadLimit
-XX:-UseGCOverheadLimit boolean. По умолчанию true. В случае если подавляющее количество времени (98%) выполняется работа GC - значительно большее время приложение простаивает, чем работает - то JVM превентивно выкидывает OutOfMemoryError. Если отключить (false), то JVM будет продолжать работу.
GC
Существует большое количество GC, которые могут подходить под различные задачи, имеют большое количество параметров для оптимизации.
Serial GC
Для управления Young и Old Generation используется один поток. Данный GC подойдет для приложений с маленькими объемами данных (например, для IoT), и работающих на машинах с одним процессором. Для применения: -XX:+UseSerialGC boolean (по умолчанию false)
Parallel GC и ParallelOld GC

Parallel GC управляет Young и Old Generation в несколько потоков, по принципу Stop-the-World. При этом, Major GC выполнялся в один поток для Old Generation, кто заметно замедлял процесс GC. Ответом на данную проблему стал ParallelOld GC. Могут быть полезными, когда пиковые нагрузки являются максимальным приоритетом над плавностью и стабильностью работы.
ParallelOld GC. Небольшое дополнение к родоначальнику Parallel GC - очистка Old Generation стала выполняться в несколько потоков. В н.в., разница между этими GC устранена и они являются эквивалентами друг друга. Для маркировки используется принцип конкурентного Mark Sweep.
Для применения: -XX:+UseParallelGC boolean или -XX:+UseParallelOldGC boolean (по умолчанию - false).
NewRatio
-XX:NewRatio number. По умолчанию 2. Пропорция использования областью памяти Old Generation всего размера Java Heap. По умолчанию, равен 2, что указывает на необходимость выделения 66.6% от всего объема Java Heap области памяти Old Generation, и оставшиеся 33% для области Young Generation. При изменении на значение равное 1, области памяти Old и Young Generation получат одинаковый размер, по 50%. При значении равном 3: Young = 25%, Old = 75%; 4: Young = 20%, Old = 80%; и так далее.
Если в приложении создается много мелких и короткоживущих объектов, то передача значения 1 позволит сократить частоту выполнения Minor GC, который полностью замораживает работу GC.
NewSize и MaxNewSize
-XX:NewSize number и -XX:MaxNewSize number. Управляют границами для области Young Generation. Задаются в байтах.
SurvivorRatio
-XX:SurvivorRatio number. По умолчанию 8. Определяет соотношение одной Survivor относительно Eden в Young Generation области. Поскольку Survivor делится на S0 и S1, вместе они буду занимать SurvivorRatio / 2 от всей области Young Generation. Так, для значения по умолчанию 8 каждая часть Survivor будет занимать 12.5% от всей области Young Generation
MaxGCPauseMillis
-XX:MaxGCPauseMillis=number. По умолчанию 200. Определяет максимальную время для сборки мусора в рамках одной итерации. Но уменьшение максимально допустимой паузы может привести к увеличению частоты их запуска.
GCPauseIntervalMillis
-XX:GCPauseIntervalMillis=number. По умолчанию 201. Определяет временное окно, в рамках которого несколько итераций сборки мусора не должны превышать заданный лимит MaxGCPauseMillis. Поскольку по умолчанию разница составляет 1 мс., каждая итерация будет рассматриваться как выполняемая в отдельном окне - в этом случае, GCPauseIntervalMillis не будет накладывать никакого эффекта. Но если окно задать меньше, то продолжительность сборки мусора будет заметно сокращена, но потребность в частоте ее выполнения так же может заметно вырасти!
GCTimeRatio
-XX:GCTimeRatio=number. По умолчанию 12. Требование к JVM, чтобы приложение не проводило времени на сборку мусора более чем 1 / (1 + GCTimeRatio).
ParallelGCThreads
-XX:ParallelGCThreads. Определяет количество потоков для параллельных фаз сборки мусора. Текущее значение можно получить:
1 | java -XX:+PrintFlagsFinal -version | grep ParallelGCThreads |
Ответ будет примерно таким:
1 | uint ParallelGCThreads = 4 {product} {default} |
Общая рекомендация такая - если CG выполняется медленно при простаивающем процессоре - поднимаем; если запущено несколько JVM, то значение опускаем для второстепенных Java приложений.
Concurrent marks and sweep collector
GC, который был удален в Java 14. Актуален для приложений, которые допускают выделение части ресурсов сборщику мусора, но это же позволяет сократить паузы между итерациями Minor и Major GC.
Для применения: -XX:+UseConcMarkSweepGC boolean (по умолчанию - false).
G1
На момент написания (Java 25) является GC по умолчанию, при доступных ресурсах 1.79GB памяти и не менее двух процессоров. Среди всех GC в Java считается довольно требовательным к ресурсам. Одновременно, является и довольно универсальным, поскольку пытается соблюдать баланс между пропускной способностью и возникающими задержками на GC.
Для применения: -XX:+UseG1GC boolean (по умолчанию - true).
Архитектура

Распределение объектов в памяти имеет некоторую специфику и отличается от методики расположения их в предыдущих реализациях Java Heap. Распределение памяти, которое применялось ранее подразумевало использование трех областей, каждая из которых выполняла свою определенную функцию. Они также (как правило) имели разную размерность.
Для G1 GC применена иная стратегия - как и ранее существуют три, точно таких же, роли области памяти - Eden Space, Survivor Space и Old Generation. Для каждой роли области памяти не существует одной большой области, которая используется исключительно ей, вместо этого, Java Heap разбита не небольшие сектора одинакового размера, и за каждым сектором, в свою очередь, закреплена та или иная роль (зависит от лимитов, устанавливаемых при старте JVM). Данные в каждом секторе отслеживаются на референтность, и, при возникновении возможности очистки GC, данные из сектора высвобождается не меняя роли сектора, и он вновь готов принимать в себя новые данные. Это помогает достигать максимальной оптимизации памяти - она высвобождается быстрее, а также, минимизирует эффект от дефрагментации выделенной памяти. G1 GC использует накопительный алгоритм и лимиты на частоту запуска GC, чтобы ограничить частоту такой дорогой операции.
Поскольку сектора при такой архитектуре памяти небольшие, бывает такое, что уместить в одном секторе все связанные между собой объекты не удается (для быстрого поиска связанных объектов). Для этого, в G1 GC используется специальная (служебная) таблица Remembered Set, RSet, в которой хранятся ссылки объектов из разных секторов.
Стоит отметить, что принципы пропорциональности для областей памяти остались в реализации G1 GC - для каждого типа (роли) области памяти (Eden Space, Survivor Space и Old Generation) можно задавать из объем относительно выделенного Java Heap.
Mixed GC
На замену ранее использовавшемуся Minor GC и Major GC, пришла новая реализация алгоритма очистки мусора - Mixed GC. Ее принцип заключается в том, что теперь очистка выполняется одновременно для Young и для Old Generation. После того, как объекты в секторе памяти Old Generation потеряли ссылки и должны быть очищены, они маркируются процессом Concurrent Marking (с одной стороны, выполняется для того чтобы не перебирать всю Java Heap в поисках кандидатов на удаление, и для избегания чрезмерно частого выполнения очистки мусора, с другой). По достижении предельного порога InitiatingHeapOccupancyPercent, IHOP на объем Old Generation, запускается процедура Mixed GC. Для очистки выбираются те сектора (вне зависимости от роли сектора), которые имеют наибольшее количество мусорных данных (очистка сектора может быть частичной). Процедура очистки будет выполняться столько итераций, сколько потребуется для достижения определяемого нижнего предела.
Отрицательным аспектом является то, что G1 GC все еще использует Stop The World принцип - когда работа JVM полностью замораживается.
UseTransparentHugePages
-XX:-UseTransparentHugePages boolean. По умолчанию true. Разрешает JVM использовать THP
ParallelRefProcEnabled
-XX:+ParallelRefProcEnabled boolean. По умолчанию true. Активизирует параллельную обработку ссылок при GC.
G1NewSizePercent и G1MaxNewSizePercent
-XX:G1NewSizePercent number и -XX:G1MaxNewSizePercent number. По умолчанию 5 и 60. Управляют границами для области Young Generation при адаптивной настройке(!). Применимо для приложений с высокой нагрузкой, но при этом, может увеличиться время очистки CG (или их частота, если ограничится максимальную продолжительность MaxGCPauseMillis).
G1HeapRegionSize
-XX:G1HeapRegionSize=number. По умолчанию можно проверить:
1 | java -XX:+PrintFlagsFinal -version | grep G1HeapRegionSize |
Размер одного региона памяти. Чем значение больше, тем выше вероятность того, что весь объект будет помещен в один регион памяти, а значит, время на поиск связанных данных объекта будет меньше - это ускорит GC. Обратной стороной является вероятная фрагментация памяти - при которой данные некоторые регионы будут полу-пустыми, а значит использование памяти будет неэффективным.
G1MixedGCLiveThresholdPercent
-XX:G1MixedGCLiveThresholdPercent number. По умолчанию 85. Определяет максимальный процент активных объектов (на которых есть внешние, не цикличные ссылки - island of isolation), в секторе памяти типа (роли) Old Generation, при котором или ниже которого этот сектор может быть кандидатом на выполнение сборки мусора Mixed GC - считается, что в нем можно освободить часть памяти. Чем выше значение, тем более лояльным к мусору в секторе становится GC - это снизит нагрузку на процессор, но увеличит количество мусора.
G1HeapWastePercent
-XX:G1HeapWastePercent number. По умолчанию 5. Допустимый порог мусорных данных, при котором сборка мусора Mixed GC может остановиться, если она ранее не была остановлена по таймауту, или за неимением мусорных объектов.
InitiatingHeapOccupancyPercent
-XX:InitiatingHeapOccupancyPercent number. По умолчанию 5. Процент заполненности Old Generation памяти, при котором следует начинать процесс Concurrent Marking - выявлять и маркировать сектора памяти. которые подпадают кандидатов на очистку. Сам процесс Concurrent Marking призван облегчить поиск кандидатов среди секторов на очистку, поскольку он не провоцирует STW, а отбирает лишь незначительные ресурсы процессора.
G1OldCSetRegionThresholdPercent
-XX:G1OldCSetRegionThresholdPercent number. По умолчанию 10. Максимальный процент памяти от всей Java Heap, который может быть собран Mixed GC на очистку в рамках одной итерации. Как было упомянуто выше, итерации будут повторяться до тех пор. пока не будет достигнут необходимая нижняя граница используемой Old Generation памяти. Очистка итеративная, поскольку очищать слишком большое количество секторов может быть слишком затратно.
G1RSetUpdatingPauseTimePercent
-XX:G1RSetUpdatingPauseTimePercent number. По умолчанию 10. Количество времени в процентном отношении от максимально допустимого времени на сборку мусора - MaxGCPauseMillis - которое GC будет выделять для обновления ссылок в RSet. Если MaxGCPauseMillis=200, то по умолчанию, GC может потратить не более 20 мс. на обновление RSet. При нарушении консистентности таблицы, производительность может упасть, т.к. поиск объектов путем перебора будет значительно дольше, чем локализация по заранее известному адресу.
Оптимизация G1
Оптимизировать распределение областей памяти может производится в следующей последовательности конфигураций:
- с целью недопущения раздувания Java Heap можно определить его жестко:
-Xms=-Xmx, а также заранее выделить весь объем и очистить его путем задания AlwaysPretouch:-Xms2048m -Xmx2048m -XX:+AlwaysPreTouch; - подключить UseLargePages:
-XX:+UseLargePages true; - попробовать разные варианты для MaxGCPauseMillis:
-XX:MaxGCPauseMillis=100; - чем динамичнее создаются новые короткоживущие объекты, тем больше следует выделять Young Generation секторов;
- если Eden и Survivor области часто приближаются к своей максимальной границе, это может сигнализировать необходимость увеличения G1MaxNewSizePercent;
- изменяя
GCPauseIntervalMillisвыбрать допустимое окно для итераций для сборки мусора; G1RSetUpdatingPauseTimePercentпоможет в сокращении времени сборки мусора (а значит, сократит STW), но может увеличить время, необходимое во время работы.
ZGC
Архитектура GC, задачей которой является минимизация задержек между паузами (STW) для сбора мусора. Заявленная цель - длительность итерации сборки мусора не более 10 мс. Также, данный GC должен стабильно работать с любым объемом памяти (хотя, для приложений с маленьким объемом выделенной памяти, данный GC подходит плохо). Считается стабильным (но все еще экспериментальным) для Java 15.
Ключевым ограничением для GC является то, что предъявляются повышенные требования к CPU.
Стоит отметить, что данный GC имеет все шансы стать следующим GC по умолчанию для JVM. По этой причине следует уделить ему особое внимание.
Для применения: -XX:+UseZGC boolean (по умолчанию - false).
Архитектура
Подход с Young Generation и Old Generation не актуален для данного GC. Память разбита на несколько регионов:
- Allocated. В данной области хранятся новые объекты. Аналог Young Generation;
- Relocated. “Выжившие” объекты - те, на которые существуют внешние (не цикличные - island of isolation) ссылки. Аналог Old Generation;
- Remapped. Объекты, которые обновили указатели в памяти.
Для каждого объекта выделяются несколько служебных бит (bit), в которых регистрируется их маркировочное состояние. Это состояние помогает избежать состояния STW и чистить память параллельно с работой приложения. Эти маркировочными состояниями могут быть следующими:
- Marked. Объект “живой”;
- Remarked. Указатель на объект менялся;
- Finalizable. Объект можно удалять.
По мере своей жизни, объект сталкивается с 3 фазами работы CG:
- Marking. Поиск и маркировка “живых” объектов;
- Relocation. Перемещение объектов в различные регионы (Allocated, Relocated, Remapped). С целью снижения вероятности “зависания” ссылок на объекты, при перемещении объектов по разным регионам памяти (Allocated, Relocated, Remapped), реализован механизм замков/барьеров (read barriers). Чтобы различные потоки верифицировали цвет (color) объекта, используется STW, но он очень короткий. Аналогично RSet в G1 используется служебная таблица (Forwarding Table), в которой при перемещении объекта, ссылки на него связываются по принципу новая-старая. Если ссылки “висячие” объекты все же остаются, они будут исправлены либо GC при итерации, либо при попытке обращении одного из потоков к объекту;
- Remapping. Обновление ссылок на перемещенные объекты.
ZGC старается не замораживать JVM для очистки памяти, работая с ним параллельно, однако, иногда GC вынужден выполнять кратковременные блокировки (STW).
Colored Pointers
Вся память в Java Heap разбита на сектора (в терминологии OJDK - страницы, pages) из допустимых 3 видов - маленькие, средние, и большие. Маленькие и средние могут хранить в себе несколько объектов (но небольших); большие могут хранить только 1 объект, который занимает весь сектор.
Для x64 систем, указатель на память состоит из 64-bit, которые располагаются на листах памяти. Размерность секторов определяется следующими значениями, зависящими от размерности Java Heap:
| Маленький | Средний | Большой | |
|---|---|---|---|
| <= 128m | 2MB | - | - |
| <= 256m | 2MB | 4MB | > 4MB |
| <= 512m | 2MB | 8MB | > 8MB |
| <= 1024m | 2MB | 16MB | > 16MB |
| > 1024m | 2MB | 32MB | > 32MB |
Указатель на адрес может структурирован следующим образом:

- 18 bit - неиспользуемые;
- F, Finalizable (0b1000) - объект мертв;
- R, Remapped (0b0100);
- M1, Marked1 (0b0010);
- M0, Marked0 (0b0001);
- Адрес в ячейке памяти (42 bit) - ссылка на физическую ячейку в памяти.
Флаги F, R, M1, M0 служат для идентификации статуса объекта - colorification. Как упоминалось ранее, в каждый момент времени может быть только одно состояние объекта, которые и описывается этими мета-данными (именуемыми цветами, colors). R, M1, M0 являются (условно) хорошими - объект жив; F “плохой”” - объект мертв. В моменты маркировки объектов, JVM выполняет стандартный STW, что гарантирует валидность состояния.
Barriers
В момент получения объекта Java кодом, например: Integer a = myObject.myVariable;, JVM отдает ему физическую ссылку на объект (хороший) объект из Java Heap. Для гарантии чтения валидного объекта из Java Heap (для получения правильной ссылки на хороший объект), используется принцип барьеров, которые разделяются на несколько видов: Load Barrier, LB - в примере выше, это myObject и Mark Barrier, MB выше это myVariable. Если объект хороший, JVM получает ссылку на объект простым способом (что очень быстро). Если же объект плохой, сперва будет выполнена проверка на факт релокации объекта и получение ссылки для релоцированного объекта.
Важно отметить, что барьеры активизируются только тогда, когда JVM использует ссылку на объект в Java Heap. Например, в примерах ниже барьеры участвовать не будут:
1 | TheObject a = myObject.getMyVariable(); // используется ссылка на метод |
Жизненный цикл
Каждый цикл ZGC состоит из следующей последовательности действий:

Для простоты восприятия лучше сразу рассматривать весь жизненный цикл на примере:

STW1

1.1. Цикл начинается с STW. Это выполняется для актуализации статусов объектов для всех потоков. На этом этапе для объектов проставляется маркировка M0/M1 - производится переключение, т.е. если в предыдущую итерацию было у объекта маркировка была M0, то теперь будет M1 (и наоборот);
1.2. Создание новых страниц (pages) для того, чтобы аллоцированные объекты помещались уже в новые страницы. Все объекты, которые будут находиться на свежих страницах, будут рассматриваться как активные в рамках текущего GC цикла. Очистка от мусора на новых страницах не выполняется, т.к. считается, что объекты там будут всегда живые. Как и ссылки на объекты на этих страницах всегда считаются живыми;
1.3. Страницы, которые существовали до текущего цикла GC, считаются релоцированными;
1.4. Маркировка (mark) параллельно (в несколько потоков) флагам M0/M1 всех корневых объектов (загруженные классы, константы и т.п.), а также всех объектов, которые участвуют в текущем стеке (Stack Frame).
Итогом выполнения данной фазы является то, что:- Свежие страницы выделены;
- Все объекты с “хорошими” цветами поменяли свои флаги (например, с M0 на M1);
- Все корневые объекты промаркированы как активные, с соответствующими цветами;
Marking/Remapping, M/R

2.1. Поскольку данная фаза длительная и конкурентная (между потоками GC и мутаторами mutators), то каждый из них может параллельно изменять статус объектов:
2.1.1. Потоки GC
2.1.1.1. GC получает список промаркированных объектов, полученный на предыдущем этапе (STW1);
2.1.1.2. Для каждого объекта на странице (page) выполняется маркировка его активности;
2.1.1.3. Обновление информации (обновляется количество живых байт) по ассоциированным со списком страниц (pages) памяти - эта информация будет впоследствии использоваться для эвакуации объектов (перенос их на те другие страницы (pages) памяти с целью дефрагментации);
2.1.1.4. К каждому помеченному объекту применяется Mark Barrier. Также, просматриваются все ссылки внутри этого объекта и, если они имеют “плохой” цвет, то они либо добавляются в forwarding tables, либо им просто присваивается “хороший” цвет;
2.1.2. Мутаторы
2.1.2.1. По мере работы с объектами, могут возникать два режима работы:
- быстрый путь - при котором указывается объект с “хорошим” статусом;
- медленный путь - объект помечается как обязательный для переадресации;
2.1.3. Объекты на страницах помещаются в соответствующие mark stack, которые последовательно разбираются и актуализируются состояния своих объектов;STW2

3.1. Служит для безопасного завершения фазы маркировки. Для этого запрещает мутаторам изменять и добавлять объекты;
3.2. Проверяет разобраны ли все mark stack. Важно отметить, что попытка входа в данную фазу GC может входить несколько раз за цикл. Это связано с тем, что опрос мутаторов (mutator) на наличие объектов для маркировки может выполняться несколько раз;Reference Processing, RP

4.1. Фаза дополняет M/R, обрабатывая Soft, Weak и Phantom связи между объектами - специальные классы-обертки в Java из
java.lang.ref, которые дают возможность работать с GC;
4.2. Для организации синхронизации между GC (который очищает ссылку) и мутаторами, используется блокировка Compare-and-Swap, CAS;Evacuation Candidates, EC selection

5.1. задачей фазы является составление списка страниц (pages) памяти, которые редко заполняются;
5.2. Выполняется очистка набора EC с предыдущей итерации GC, а также очищаются таблицы переадресации (forwarding tables);
5.3. Выполняется перебор всех страниц (pages) памяти и, если на странице нет ни одного объекта, она немедленно высвобождается; если есть хотя бы один - она помечается как EC, но только предварительно;
5.4. Все полученные страницы сортируются по количеству байт в них (от меньшего большему). Те, которые находятся в нижней части списка, считаются страницами (pages) памяти с большим количеством активных объектов;
5.5. Страницы, которые содержат только один объект не будут релоцироваться (это дорого и бессмысленно) - таким образом, в EC списке остаются только маленькие и средние объекты;STW3

6.1. Выполняется обход по всем корневым объектам. Если объект ссылается на какую-то страница (page) памяти, находящуюся в EC списке, объект будет перемещен в область памяти, которая не является EC; если у них нет такой ссылки - они просто помечаются “хорошим” цветом (R). Но так или иначе, все корневые объекты получат “хороший” цвет (а значит, не будут EC);
6.2. Объекты, которые указывают на EC, помечаются флагом R - Remapped (0b0100);Relocation, RE

7.1. Выполняется несколькими потоками GC параллельно (используя атомарные Compare-and-Swap, CAS), а также мутаторами, но только в том случае, если требуется доступ к объекту в процессе его перемещения;
7.2. Потоки GC проходят по всем страницам, помеченным как EC;
7.3. Для каждого живого объекта на странице (page) памяти, помеченной EC, выделяется память на новых страницах;
7.4. Данный объект копируется из EC на новую страницу. Фиксируется запись о перемещении в таблицу forwarding tables;
7.5. Во время работы потоков GC, объект может быть запрошен мутаторами. В этом случае, они рассматривают цвет объекта как “плохой” и потребуют выполнить его релокацию. Для этого они так же используют forwarding tables;Повтор всего цикла

8.1. Важно отметить, что очистка старой страницы (page) памяти происходит на фазе Evacuation Candidates, EC!
Shenandoah GC
Архитектура GC, которая призвана выполнять ту же функцию, что и ZGC - заменить G1 GC.
Для применения: -XX:+UseShenandoahGC boolean (по умолчанию - false).
Этот раздел в разработке.
Epsilon GC
“Ленивый” GC, который не выполняет очистку мусора, а в случае переполнения GC, он просто упадет с ошибкой OutOfMemoryError. Для чего же он тогда нужен? Очевидно, что для большинства приложений в продуктивной среде такое использоваться не будет. Но поскольку очистки памяти нет, нет и задержек, вызванных необходимостью аллокации, маркировки объектов, и т.п. Это делает из Epsilon GC идеальную среду для тестирования кусков функционала, замер производительности, оценки потребление памяти объектами и для прочих служебных задач. Более того, некоторые типы приложений могут запускаться кратковременно, быстро выполняя определенную задачу - тут данная GC также покажет себя лучше всех остальных.
Для применения: -XX:+UseEpsilonGC boolean (по умолчанию - false).
Этот раздел в разработке.
Логгирование GC
Дополнительное логгирование GC позволяет выявлять “раздувание” Java Heap, анализировать микропаузы в работе JVM и выявлять проблемы с производительностью.
1 | –verbose:gc |
ы
либо
1 | -XX:PrintGCDetails |
Программа, для которой будет представлен вывод лога будет ниже:
1 | public class Main { |
Пример вывода:
1 | [0.005s][info][gc] Using Serial |
-XX:+UseSerialGC -Xms1024m -Xmx1024m -verbose:gc