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

Що таке stop-the-world

Головна мета STW — гарантія консистентності графу пам'яті:


🟢 Junior Level

Stop-The-World (STW) — це спеціальний стан віртуальної машини Java, за якого виконання абсолютно всіх прикладних потоків застосунку (Mutator Threads) примусово призупиняється. У цей час процесорний час повністю передається системному потоку віртуальної машини (VM Thread) для виконання критично важливих системних операцій.


Навіщо потрібен STW?

Головна мета STW — гарантія консистентності графу пам’яті:

  • Якби потоки застосунку продовжували змінювати посилання та створювати об’єкти в той момент, коли збирач сміття обходить пам’ять, адреси об’єктів постійно мутували б просто під час роботи GC.
  • Побутова аналогія: Групова фотографія на весіллі. Щоб знімок вийшов чітким, фотограф вигукує: «Замріть на секунду!». Якщо гості продовжать ходити, знімок вийде розмитим, а хтось узагалі випаде з кадру.

Чи викликається STW лише збирачем сміття?

НІ! Це одне з найулюбленіших запитань на співбесідах. STW потрібен не лише для Garbage Collector, але й для цілої низки внутрішніх операцій JVM:

  1. Зняття Thread Dump: Отримання знімка стеків усіх потоків (через jstack або VisualVM).
  2. Відкликання зміщених блокувань (Biased Lock Revocation): До Java 15 перемикання монітора синхронізації вимагало зупинки потоку.
  3. Деоптимізація коду: Заміна скомпільованого JIT машинного коду назад на байткод інтерпретатора в разі порушення оптимістичних спекуляцій компілятора.
  4. Гаряча заміна класів (HotSwap / Class Redefinition): Перезавантаження байткоду класів через JVM Tool Interface (JVMTI) під час налагодження.

🟡 Middle Level

Механізм безпечних точок (Safepoints)

Потоки застосунку не можуть зупинитися миттєво на довільній асемблерній інструкції (наприклад, посеред створення об’єкта чи між обчисленням операндів). Вони зобов’язані дійти до так званої Безпечної точки (Safepoint).

Safepoint — це інструкція в машинному коді, в якій стан стека потоку є повністю узгодженим, а всі покажчики на об’єкти в регістрах і стеку зафіксовані у спеціальній карті (OopMap).

┌────────────────────────────────────────────────────────┐
│               ДЕ JIT РОЗСТАВЛЯЄ SAFEPOINTS:            │
├────────────────────────────────────────────────────────┤
│ 1. У точках повернення з методів (Method Exit).        │
│ 2. На зворотних переходах незліченних циклів (Backedge)│
│ 3. Перед інструкціями алокації пам'яті (new).          │
│ 4. При переходах між Java та нативним кодом (JNI).     │
└────────────────────────────────────────────────────────┘

Як апаратно влаштоване опитування (Safepoint Polling)

Щоб зупинити потоки, VM Thread не надсилає кожному потоку дорогий системний сигнал ОС:

  1. JIT вставляє в кожну точку Safepoint ультрашвидку інструкцію читання з тестової сторінки пам’яті (Safepoint Polling Page):
    test %eax, 0x160(%r15)   ; Читання зі спеціальної сторінки опитування
    
  2. У нормальному режимі: Сторінка доступна для читання. Читання відбувається миттєво (влучання в кеш L1, затримка $\approx 1$ такт процесора).
  3. Коли потрібен STW: JVM через системний виклик mprotect робить сторінку недоступною для читання (PROT_NONE).
  4. Наступний же потік, який виконує читання, спричиняє апаратний виняток процесора SIGSEGV (Page Fault). Системний обробник сигналів JVM перехоплює його і переводить потік у режим очікування (Sleep).

Час входу в паузу: TTSP (Time-To-Safepoint)

Загальна тривалість затримки сервісу складається з двох фаз: \(\text{Total Pause Time} = \text{TTSP (Time To Safepoint)} + \text{Operation Time (GC Execution)}\)

  • TTSP — це час від моменту, коли JVM зажадала зупинки, до моменту, коли останній найповільніший потік застосунку дійшов до свого Safepoint.
  • Усі інші потоки, що дійшли до точки першими, вже стоять і чекають.

🔴 Senior Level

Проблема зліченних циклів (Counted Loops Bug)

Класичний сценарій багатосекундних «зависань» високонавантажених систем:

// ❌ COUNTED LOOP: мільйони ітерацій без Safepoint!
for (int i = 0; i < 1_000_000_000; i++) {
    sum += array[i]; // JIT агресивно оптимізує цикл int і вирізає safepoint poll!
}

Чому це відбувається:

JIT-компілятор (C2) припускає, що цикл із лічильником типу int є коротким. Щоб виконати векторизацію (SIMD) та інлайнінг, компілятор повністю видаляє перевірку Safepoint з тіла циклу.

Якщо такий цикл крутиться 2 секунди, вся JVM буде заблокована на 2 секунди, навіть якщо саме збирання сміття займає всього 2 мілісекунди! У логах GC це виглядає так:

[GC(12) Total time for which application threads were stopped: 2.0520000 seconds, Stopping threads took: 2.0490000 seconds]

(Саме збирання сміття зайняло 3 мс, а зупинка потоків — понад 2 секунди!)

Способи розв’язання проблеми Counted Loops:

  1. Увімкнути примусові безпечні точки у зліченних циклах:
    -XX:+UseCountedLoopSafepoints
    
  2. Замінити тип змінної циклу з int на long (for (long i = 0; ...)): для циклів із long JIT не вважає цикл лічильним і гарантовано зберігає Safepoints.

Локальні рукостискання: Thread-Local Handshakes (Java 10+, JEP 312)

До Java 10 будь-яка операція, що вимагала взаємодії навіть з одним потоком (наприклад, зняття стека окремого потоку), викликала глобальний Stop-The-World для всіх сотень потоків у процесі.

У Java 10 представлено механізм Thread-Local Handshakes:

  • JVM нарізає персональні сторінки опитування для кожного потоку окремо.
  • Віртуальна машина може ініціювати рукостискання (Handshake) з одним конкретним потоком, зупинивши лише його, тоді як решта сотень потоків застосунку продовжують обробляти бойовий трафік.
  • Це радикально знизило кількість глобальних STW-пауз у JVM.

JNI та безпечні точки

Часте запитання: «Що відбувається з потоком, який виконує нативний C/C++ код через JNI під час STW?»

  • Потік, що перебуває всередині нативного JNI-методу, не зупиняється під час фази STW! Оскільки він виконує нативний код, JVM знає, що він не може маніпулювати об’єктами Java Heap безпосередньо без JNI-функцій.
  • Однак у момент, коли нативний метод спробує повернутися назад у Java-код або викликати JNIEnv-функцію, бар’єр JNI виявить активний Safepoint і заблокує потік до завершення фази STW.

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

30-секундна відповідь (Elevator Pitch)

«Stop-The-World (STW) — це повне призупинення всіх прикладних потоків застосунку для виконання системних операцій JVM, що вимагають консистентного стану пам’яті. STW використовується не лише для фаз Garbage Collection, а й для зняття Thread Dumps, відкликання блокувань та деоптимізації JIT-коду. Потоки зупиняються у спеціальних точках — Safepoints, опитування яких виконується через перевірку захищеної сторінки пам’яті. Ключова прихована проблема затримок — TTSP (Time-To-Safepoint): якщо один потік застряг у лічильному циклі без безпечних точок (Counted Loop), усі інші потоки чекатимуть його секундами (вирішується прапорцем -XX:+UseCountedLoopSafepoints)».


3–4 каверзних питання з відповідями

  1. Для яких операцій JVM, окрім збирання сміття, потрібен виклик Stop-The-World?
    • Відповідь: STW необхідний при знятті повного дампу потоків (Thread Dump через jstack), при деоптимізації скомпільованого машинного коду JIT-компілятором, при гарячій заміні байткоду класів (Class Redefinition / HotSwap через JVMTI агенти), при розширенні пулу дескрипторів та (до Java 15) при відкликанні зміщених блокувань (Biased Lock Revocation).
  2. Що таке Time-To-Safepoint (TTSP) і чому він небезпечніший за саме збирання сміття?
    • Відповідь: TTSP — це час від моменту виставлення прапорця паузи до моменту, коли останній працюючий потік дійде до найближчого Safepoint і засне. Небезпека в тому, що всі інші потоки, які вже дійшли до Safepoints, повністю заморожені й простоюють. Якщо хоча б один потік застряг у довгому циклі чи блокуючому системному виклику вводу-виводу, час загального зависання системи може становити секунди, хоча сам збирач сміття відпрацює за пару мілісекунд.
  3. Чому в циклах for (int i = 0; i < N; i++) за замовчуванням немає Safepoint?
    • Відповідь: Щоб не знижувати продуктивність числових обчислень. Додавання інструкції читання зі сторінки пам’яті на кожній ітерації циклу блокує векторизацію (SIMD-інструкції) та інлайнінг. JIT оптимізує такі лічильні цикли (Counted Loops), вважаючи, що вони завершаться швидко. Для запобігання затримкам TTSP на довгих обчисленнях використовують прапорець -XX:+UseCountedLoopSafepoints.
  4. Як механізм Thread-Local Handshakes у Java 10+ знизив вплив пауз на систему?
    • Відповідь: До появи Handshakes будь-яка операція над потоком вимагала глобальної зупинки всієї JVM. Механізм Thread-Local Handshakes дозволив адресно виставляти захисні бар’єри індивідуальним потокам. Тепер такі операції, як зняття стека одного потоку, відкликання блокування чи зупинка потоку профілювальником, виконуються точково, не зачіпаючи решту паралельних потоків застосунку.

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

  • 🚩 Твердження: «Stop-The-World викликається ТІЛЬКИ збирачем сміття» (STW використовується для thread dumps, деоптимізацій, зміни класів).
  • 🚩 Сліпа віра в те, що «пауза GC дорівнює часу роботи самого алгоритму збирача» (час TTSP часто перевищує час самого GC).
  • 🚩 Омана: «JNI-потоки миттєво блокуються на початку STW» (JNI-потік продовжує роботу в нативному коді й блокується лише при спробі повернутися в Java).
  • 🚩 Ігнорування проблеми Counted Loops під час розслідування нерегулярних сплесків Latency (Spikes).

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