🧠 Розділ 3 · Питання #18

Що таке параметри -Xms та -Xmx

Прапорці -Xms та -Xmx — це базові параметри віртуальної машини Java (JVM), що керують розміром купи (Java Heap), у якій виділяється пам'ять під усі об'єкти:


🟢 Junior Level

Прапорці -Xms та -Xmx — це базові параметри віртуальної машини Java (JVM), що керують розміром купи (Java Heap), у якій виділяється пам’ять під усі об’єкти:

  • -Xms<size> (Memory Start): початковий (мінімальний) розмір купи при запуску JVM.
  • -Xmx<size> (Memory Maximum): максимально дозволений розмір купи. При спробі застосунку перевищити цей ліміт (і якщо Garbage Collector не зміг звільнити пам’ять) викидається помилка java.lang.OutOfMemoryError: Java heap space.
# Запуск застосунку з початковою купою 1 ГБ та максимальною 4 ГБ
java -Xms1g -Xmx4g -jar app.jar

Суфікси одиниць виміру

  • k або K — кілобайти
  • m або M — мегабайти
  • g або G — гігабайти
java -Xms512m -Xmx2g -jar app.jar

Значення за замовчуванням (Ergonomics)

Якщо прапорці не задані явно, JVM розраховує їх автоматично через механізм JVM Ergonomics, спираючись на доступну фізичну RAM хоста або ліміти cgroups (контейнера):

  • За замовчуванням -Xms становить приблизно $1/64$ від фізичної пам’яті.
  • За замовчуванням -Xmx становить приблизно $1/4$ від фізичної пам’яті (але зазвичай не більше 32 ГБ через обмеження Compressed OOPs).

🟡 Middle Level

1. Механіка пам’яті: Reserved vs Committed

При запуску JVM з параметрами -Xms та -Xmx відбуваються два фундаментальні етапи взаємодії з пам’яттю операційної системи:

Віртуальний адресний простір ОС:
[================== Reserved (-Xmx: 4 GB) ==================]
[=== Committed (-Xms: 1 GB) ===][........ Uncommitted ......]
  ↑ Реальні сторінки RAM          ↑ Адреси зарезервовані,
                                    але сторінки не виділені
  1. Reserved (Зарезервована пам’ять): Під час старту JVM виконує системний виклик (mmap у Linux/POSIX із прапорцем PROT_NONE або VirtualAlloc із MEM_RESERVE у Windows) на повний розмір -Xmx. Це неперервний діапазон адрес віртуальної пам’яті, що резервується за процесом, аби купа могла зростати неперервно. Фізична RAM при цьому ще не витрачається.
  2. Committed (Зафіксована пам’ять): Пам’ять в обсязі -Xms позначається як доступна для читання та запису (PROT_READ | PROT_WRITE або MEM_COMMIT). ОС виділяє реальні фізичні сторінки оперативної пам’яті в міру першого звернення до них (lazy allocation / demand paging).

2. Чому в production рекомендується -Xms = -Xmx?

Якщо -Xms < -Xmx, купа динамічно зростає та стискається залежно від навантаження, орієнтуючись на прапорці -XX:MinHeapFreeRatio (за замовчуванням 40%) та -XX:MaxHeapFreeRatio (за замовчуванням 70%):

  • Коли частка вільної пам’яті після GC падає нижче MinHeapFreeRatio, JVM намагається розширити купу до -Xmx.
  • Перед розширенням купи JVM часто ініціює додаткове збирання сміття (аж до Full GC у традиційних збирачах), щоб спробувати звільнити місце без звернення до ОС.
  • Сам системний виклик mmap/mprotect/VirtualAlloc та виділення сторінок ОС породжують Stop-The-World паузи та латентність.

Переваги рівності -Xms = -Xmx:

  • Передбачуваність: розмір купи статичний з першої секунди роботи;
  • Відсутність пауз на ресайз: JVM не витрачає процесорний час і не гальмує потоки застосунку запитами на розширення купи до ядра ОС;
  • Запобігання OOM Killer: якщо на хості закінчиться пам’ять через 5 годин під навантаженням, купа, що динамічно росте, призведе до аварійного завершення процесу операційною системою. Статичний розмір гарантує, що якщо пам’ять виділилася на старті, застосунок продовжить роботу.

Винятки: CLI-утиліти, локальна розробка, безсерверні функції (AWS Lambda) та мікросервіси в динамічних кластерах, де пріоритетом є мінімальне споживання RAM під час простою.

3. Налаштування пам’яті в контейнерах (Docker / Kubernetes)

Жорстко задавати -Xms та -Xmx у мегабайтах усередині Dockerfile або deployment-маніфестів — антипатерн, оскільки при зміні лімітів пода в Kubernetes (resources.limits.memory) параметри JVM доведеться змінювати вручну.

Починаючи з Java 8u191 та Java 10+, JVM повноцінно підтримує Container Awareness (cgroups v1/v2). Для налаштування пам’яті використовують процентні співвідношення:

# Рекомендований production-шаблон для мікросервісів у контейнері:
java -XX:InitialRAMPercentage=75.0 \
     -XX:MaxRAMPercentage=75.0 \
     -XX:MinRAMPercentage=75.0 \
     -jar app.jar
  • -XX:MaxRAMPercentage=75.0: виділяє 75% від загального ліміту пам’яті контейнера під Java Heap.
  • Решта 25% життєво необхідні для Non-Heap пам’яті (Metaspace, Thread Stacks, Code Cache, GC-структури, Direct Memory) та ядра ОС.

🔴 Senior Level

1. Формула RSS: чому процес споживає більше за -Xmx

Одна з найчастіших аварій у Kubernetes — примусове завершення пода через OOMKilled (Exit Code 137), хоча метрики Heap показують, що купа заповнена лише наполовину.

Реальне споживання фізичної пам’яті процесом в ОС (Resident Set Size — RSS) розраховується за формулою:

\[\text{RSS} \approx \text{Heap (-Xmx)} + \text{Metaspace} + \text{CodeCache} + (\text{Threads} \times \text{-Xss}) + \text{DirectMemory} + \text{GC Overhead} + \text{Native Libraries}\]
+---------------------------------------------------------------+
|               Загальний процес JVM в ОС (RSS)                 |
| +-------------------------+ +-------------------------------+ |
| |        Java Heap        | |            Non-Heap           | |
| | (-Xms / -Xmx /          | | - Metaspace                   | |
| |  MaxRAMPercentage)      | | - Thread Stacks (-Xss)        | |
| |                         | | - Code Cache (JIT-код)        | |
| |                         | | - Direct Byte Buffers         | |
| |                         | | - GC Card Tables / Marks      | |
| |                         | | - Native memory (glibc malloc)| |
| +-------------------------+ +-------------------------------+ |
+---------------------------------------------------------------+
  • Thread Stacks: кожен потік за замовчуванням забирає 1 МБ пам’яті (-Xss1m). При 1000 потоках це 1 ГБ Native RAM поза купою.
  • GC Overhead: збирачі сміття зберігають службові структури даних у native memory. Наприклад, для G1 GC таблиці запам’ятованих наборів (RSet) можуть споживати додатково до 10–15% від розміру купи.
  • Direct Memory: буфери вводу-виводу (NIO ByteBuffer.allocateDirect()), які активно використовуються в Netty, gRPC, Kafka.
  • Інструмент контролю: -XX:NativeMemoryTracking=summary та утиліта jcmd <pid> VM.native_memory summary.

2. Межа 32 ГБ та Compressed OOPs

У 64-бітних архітектурах покажчики на об’єкти займають 8 байтів (64 біти), що збільшує споживання купи на 30–40% порівняно з 32-бітними системами. Для оптимізації JVM використовує технологію Compressed OOPs (Ordinary Object Pointers) (-XX:+UseCompressedOops).

JVM зберігає покажчики не як байтові адреси, а зі зсувом на 3 біти вправо (оскільки всі об’єкти в Java вирівняні за межею 8 байтів). Це дозволяє адресувати $2^{32} \times 8 = 32\text{ ГБ}$ пам’яті за допомогою 32-бітних покажчиків:

\[\text{Поріг вимкнення Compressed OOPs} \approx 32\text{ ГБ}\]

[!WARNING] Ніколи не встановлюйте -Xmx32g! Якщо задати купу рівно 32 ГБ (або 32768m), JVM виходить за діапазон адресації зі зсувом. Compressed OOPs вимикаються, і JVM переходить на повноцінні 64-бітні покажчики. У результаті купа -Xmx32g вміщує менше реальних об’єктів, ніж купа -Xmx31g, а навантаження на кеші процесора (L1/L2/L3) та пропускна здатність шини пам’яті різко деградують. Безпечний максимум для стиснутих покажчиків зазвичай становить близько 31g (або 31744m).

3. Оптимізація запуску: -XX:+AlwaysPreTouch

За замовчуванням ОС виділяє фізичну пам’ять відкладено (page fault on demand). Коли Java-застосунок запускається і починає інтенсивно обробляти запити, кожне перше звернення до сторінки пам’яті викликає переривання процесора (page fault), змушуючи ядро ОС шукати вільний фізичний фрейм і зануляти його. Це призводить до просідань латентності (Tail Latency spikes) у перші хвилини роботи застосунку.

Прапорець -XX:+AlwaysPreTouch:

  • Змушує JVM під час старту та ініціалізації купи обійти всі сторінки пам’яті й записати в кожну нульовий байт.
  • Виділення сторінок відбувається паралельно в кілька потоків (-XX:PreTouchParallelChunkSize).
  • Ціна: час старту JVM збільшується (на купі у 64 ГБ старт може тривати 5–15 секунд).
  • Вигода: повне усунення затримок через page fault під час бойової роботи сервісу.

4. Transparent Huge Pages (THP) та збирачі сміття

У Linux стандартний розмір сторінки віртуальної пам’яті дорівнює 4 КБ. Huge Pages збільшують цей розмір до 2 МБ або 1 ГБ, зменшуючи кількість записів у TLB-кеші процесора (Translation Lookaside Buffer).

Однак механізм Transparent Huge Pages (THP) ядра Linux намагається об’єднувати 4 КБ сторінки у 2 МБ на льоту за допомогою фонового демона khugepaged. Під час роботи із сучасними GC (особливо G1 GC, ZGC, Shenandoah) це викликає катастрофічні затримки:

  • G1 оперує невеликими регіонами (1–32 МБ) і часто переміщує дані.
  • Дефрагментація THP блокує системні виклики виділення пам’яті, викликаючи непередбачені паузи Stop-the-World на десятки й сотні мілісекунд.
  • Рекомендація для High-Load: вимикати THP на рівні ОС (echo madvise > /sys/kernel/mm/transparent_hugepage/enabled) або використовувати статичні Huge Pages (-XX:+UseLargePages).

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь для співбесіди

«Параметри -Xms та -Xmx задають початковий та максимальний розмір купи JVM. -Xmx визначає межу зарезервованого віртуального адресного простору (reserved), а -Xms — початково виділену фізичну пам’ять (committed). У production заведено задавати -Xms = -Xmx (або -XX:InitialRAMPercentage = -XX:MaxRAMPercentage), щоб усунути накладні витрати та GC-паузи на динамічний ресайз купи. Важливо пам’ятати, що загальний процес Java (RSS) завжди споживає більше пам’яті, ніж -Xmx, через Metaspace, Thread Stacks, Direct Buffers та службові структури GC, тому під Heap у контейнерах виділяють не більше 70–75% RAM».


Питання підвищеної складності з відповідями

  1. Що станеться, якщо задати -Xms4g -Xmx4g, але на сервері доступно лише 2 ГБ вільної фізичної пам’яті?
    • Відповідь: Без прапорця -XX:+AlwaysPreTouch застосунок успішно запуститься, оскільки Linux виділить віртуальні адреси й зафіксує їх без миттєвої прив’язки до фізичних фреймів (overcommit). Проте щойно JVM спробує фактично записати дані в пам’ять понад 2 ГБ, ядро Linux завершить процес сигналом SIGKILL через OOM Killer. Із прапорцем -XX:+AlwaysPreTouch JVM впаде одразу в момент старту з помилкою нестачі пам’яті від ОС.
  2. Чому купа -Xmx31g може бути ефективнішою, ніж -Xmx33g?
    • Відповідь: До межі ~32 ГБ JVM використовує Compressed OOPs (стиснуті 32-бітні посилання на об’єкти). При перевищенні цієї межі (починаючи приблизно з 32 ГБ) стиснення вимикається, і всі посилання стають 64-бітними (8 байтів замість 4). Це збільшує розмір графу об’єктів у пам’яті на 20–30%, вимиває процесорні кеші та погіршує продуктивність. Щоб отримати реальний виграш у корисній місткості після вимкнення Compressed OOPs, купу потрібно збільшувати відразу мінімум до 40–48 ГБ.
  3. Чому контейнер у Kubernetes падає з Exit Code 137, якщо -Xmx дорівнює ліміту пам’яті пода?
    • Відповідь: Exit Code 137 означає відправлення сигналу SIGKILL (128 + 9) системним механізмом Linux OOM Killer при перевищенні cgroups memory limit. Пам’ять процесу Java (RSS) складається з Heap та Native Memory (Metaspace, стеки потоків -Xss, CodeCache, Direct Memory, таблиці GC). Якщо -Xmx дорівнює ліміту контейнера, будь-яка non-heap алокація миттєво викличе перевищення ліміту cgroup та аварійну смерть пода. Ліміт купи повинен становити 70–80% від ліміту контейнера.

Типові помилки та Red Flags

  • 🚩 Вважати, що -Xmx — це максимальний обсяг оперативної пам’яті, який може зайняти Java-застосунок. (Red Flag: нерозуміння архітектури віртуальної машини, незнання Native Memory, Metaspace та Thread Stacks).
  • 🚩 Виділяти 95–100% пам’яті контейнера під -Xmx у Docker. (Red Flag: гарантований OOMKilled у продакшені).
  • 🚩 Стверджувати, що -Xms виділяє фізичну пам’ять одразу за будь-яких умов. (Омана: без -XX:+AlwaysPreTouch фізичні сторінки виділяються ОС ліниво при першому читанні/записі).
  • 🚩 Задавати розмір купи рівно 32 ГБ. (Незнання механіки Compressed OOPs).

Пов’язані теми