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

Як отримати heap dump

Найнадійніший і найпоширеніший підхід — налаштувати віртуальну машину так, щоб вона сама зберігала знімок пам'яті при вичерпанні ресурсів:


🟢 Junior Level

Heap Dump (дамп купи) можна отримати кількома способами: автоматично при збої застосунку, вручну через консоль або за допомогою графічних інструментів розробки.

1. Автоматично при OutOfMemoryError (Production Must-Have)

Найнадійніший і найпоширеніший підхід — налаштувати віртуальну машину так, щоб вона сама зберігала знімок пам’яті при вичерпанні ресурсів:

java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/dumps/app_oom.hprof \
     -jar app.jar

2. Вручну через консольну утиліту jcmd

Якщо застосунок працює повільно або є підозра на витік, дамп можна зняти на льоту:

# 1. Дізнатися PID процесу
jps
# або: ps aux | grep java

# 2. Зняти дамп пам'яті
jcmd <PID> GC.heap_dump /tmp/heap_dump.hprof

3. Через графічний інтерфейс (VisualVM / IntelliJ IDEA)

  • У VisualVM: знайти процес у списку ліворуч, натиснути правою кнопкою миші $\to$ Heap Dump.
  • В IntelliJ IDEA Ultimate: відкрити вкладку Profiler $\to$ вибрати запущений процес $\to$ Capture Memory Snapshot.

🟡 Middle Level

1. Просунуті параметри зняття через jcmd

Утиліта jcmd підтримує параметри стиснення та фільтрації:

# Стиснення на льоту архіватором gzip (Java 11u12+ та Java 17+)
# -gz=1 (мінімальне стиснення, мінімальне навантаження на CPU)
# -gz=9 (максимальне стиснення, менший розмір файлу)
jcmd <PID> GC.heap_dump -gz=1 /tmp/dump.hprof.gz

# Зняття ТІЛЬКИ ЖИВИХ об'єктів (ініціює попередній Full GC)
jcmd <PID> GC.heap_dump -all=false /tmp/live_dump.hprof

[!NOTE] Файл дампу купи розміром 32 ГБ при прапорці -gz=1 стискається до 6–8 ГБ. Це економить місце на диску і в рази скорочує час завантаження дампу з віддаленого сервера.

2. Програмне зняття з Java-коду

HotSpot надає офіційний JMX-бін HotSpotDiagnosticMXBean, що дозволяє запросити дамп безпосередньо з бізнес-коду:

import com.sun.management.HotSpotDiagnosticMXBean;
import java.io.IOException;
import java.lang.management.ManagementFactory;

public class DiagnosticService {

    public static void dumpHeap(String filePath, boolean liveOnly) throws IOException {
        HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(
                ManagementFactory.getPlatformMBeanServer(),
                "com.sun.management:type=HotSpotDiagnostic",
                HotSpotDiagnosticMXBean.class
        );
        // liveOnly=true викликає попередній Full GC
        mxBean.dumpHeap(filePath, liveOnly);
    }
}

3. Зняття через Spring Boot Actuator

У мікросервісах на Spring Boot доступний вбудований ендпоінт Actuator:

# Завантаження дампу купи через HTTP GET
curl -O http://localhost:8080/actuator/heapdump
  • Під капотом Actuator звертається до HotSpotDiagnosticMXBean, зберігає тимчасовий файл і передає його у вигляді HTTP-відповіді (application/octet-stream).
  • ⚠️ Ризик безпеки: цей ендпоінт зобов’язаний бути захищений авторизацією (роль ADMIN) або закритий від зовнішньої мережі, інакше зловмисник зможе завантажити всю пам’ять сервісу з токенами та паролями.

🔴 Senior Level

1. Механіка Attach API та пастки прав доступу в контейнерах

Команда jcmd взаємодіє з цільовою віртуальною машиною через HotSpot Attach API:

  1. Клієнт jcmd створює сигнальний файл у каталозі тимчасових файлів (/tmp/.java_pid<PID>);
  2. Відправляє процесу JVM сигнал SIGQUIT (3);
  3. Потік Signal Dispatcher всередині JVM активує потік Attach Listener, який відкриває Unix Domain Socket (/tmp/.java_pid<PID>);
  4. jcmd передає команду GC.heap_dump через сокет.

Типові збої в Docker / Kubernetes:

  • Помилка Unable to open socket file: У production контейнери запускаються під непривілейованим користувачем (наприклад, appuser з UID 10001). Якщо увійти в контейнер під root і виконати jcmd 1 GC.heap_dump ..., команда завершиться з помилкою прав доступу. Рішення: виконувати команду від імені того ж користувача, під яким працює процес:
    su -s /bin/sh appuser -c "jcmd 1 GC.heap_dump -gz=1 /tmp/dump.hprof.gz"
    
  • Ізоляція каталогу /tmp: Якщо /tmp змонтовано з опцією noexec або JVM запущено з прапорцем -XX:+DisableAttachMechanism, jcmd не зможе підключитися.

2. Зняття дампу з пода Kubernetes під бойовим навантаженням

Покроковий сценарій вивантаження пам’яті без падіння сервісу:

# Крок 1: Ізолювати под від вхідного трафіку (видалити лейбл селектора сервісу)
kubectl label pod order-service-7f8d-xkz pod-under-diag=true --overwrite
kubectl label pod order-service-7f8d-xkz app-

# Крок 2: Виконати команду зняття дампу зі стисненням на змонтований том
kubectl exec order-service-7f8d-xkz -- jcmd 1 GC.heap_dump -gz=1 /dumps/diag.hprof.gz

# Крок 3: Скопіювати дамп на локальну станцію інженера
kubectl cp order-service-7f8d-xkz:/dumps/diag.hprof.gz ./diag.hprof.gz

# Крок 4: Видалити тимчасовий файл дампу з пода
kubectl exec order-service-7f8d-xkz -- rm /dumps/diag.hprof.gz

# Крок 5: Перезапустити або видалити под, щоб відновити балансування
kubectl delete pod order-service-7f8d-xkz

3. Зняття дампу через GNU Debugger (gcore)

Якщо JVM повністю зависла в мертвому циклі (Thread deadlock, uninterruptible sleep, пошкодження структур даних) і не відповідає на команди Attach API (jcmd висить нескінченно), єдиним способом зняти пам’ять процесу є генерація системного core dump через ядро Linux:

# Зняття системного дампу пам'яті ядра Linux без участі JVM
gcore -o /dumps/core.dump <PID>

# Конвертація системного core dump у Java HPROF за допомогою утиліти jhsdb
jhsdb jmap --exe $JAVA_HOME/bin/java --core /dumps/core.dump.<PID> --binaryheap --dumpfile /dumps/converted.hprof

Цей підхід працює на рівні ядра ОС і дозволяє досліджувати навіть намертво завислі або впалі процеси.


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

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

«Отримати Heap Dump можна автоматично при збої через параметри віртуальної машини -XX:+HeapDumpOnOutOfMemoryError та -XX:HeapDumpPath, або вручну через консольну утиліту jcmd <PID> GC.heap_dump [-gz=1] <path>. У сучасних версіях Java рекомендується використовувати прапорець -gz=1 для стиснення дампу на льоту, що заощаджує до 75% місця. У коді дамп можна зняти через HotSpotDiagnosticMXBean або ендпоінт Spring Boot Actuator /actuator/heapdump. У контейнерах важливо пам’ятати про права користувача (UID): jcmd повинен запускатися з-під того ж користувача, що й процес Java».


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

  1. Чому jcmd <PID> GC.heap_dump усередині Docker-контейнера може видати помилку Unable to open socket file?
    • Відповідь: Attach API JVM спирається на створення Unix domain socket у каталозі /tmp/.java_pid<PID>. Якщо оператор заходить у контейнер через kubectl exec під користувачем root, а Java-процес запущено під непривілейованим користувачем (наприклад, appuser), ядро ОС заборонить доступ до сокета через незбіг прав UID/GID. Для вирішення команди jcmd необхідно виконувати від імені того самого користувача, під яким працює JVM (su -s /bin/sh appuser -c "jcmd ...").
  2. Чим небезпечний виклик /actuator/heapdump на бойовому Spring Boot сервісі?
    • Відповідь: Ендпоінт запускає синхронне зняття дампу пам’яті через HotSpotDiagnosticMXBean. Це:
      • Вводить JVM у Stop-The-World паузу на весь час формування дампу;
      • Забиває мережу та процесор передачею десятків гігабайтів потоку даних;
      • Створює ризик відмови в обслуговуванні (DoS);
      • Несе критичну загрозу безпеці: дамп містить відкриті токени, ключі шифрування та персональні дані користувачів.
  3. Що станеться, якщо під час виконання GC.heap_dump на сервері закінчиться вільне місце на диску?
    • Відповідь: Системний виклик запису на диск поверне помилку No space left on device (ENOSPC). JVM перехопить цю помилку вводу-виводу, перерве операцію збереження дампу, вийде з точки зупинки Safepoint і продовжить роботу застосунку. При цьому на диску залишиться незавершений пошкоджений .hprof файл, який не зможе бути відкритий стандартними аналізаторами без попереднього ручного відновлення.
  4. У чому різниця між зняттям дампу через jcmd та зняттям через gcore + jhsdb?
    • Відповідь: jcmd працює через механізм Attach API самої віртуальної машини HotSpot і вимагає, щоб JVM була живою і здатною зупинити потоки в точці Safepoint. gcore — це утиліта ОС Linux, що знімає повний дамп адресного простору процесу через ядро операційної системи незалежно від стану JVM. gcore застосовується в аварійних ситуаціях, коли JVM наглухо зависла на нативному рівні або в непереривному системному виклику і jcmd не відповідає.

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

  • 🚩 Рекомендувати застарілу утиліту jmap замість jcmd. (Red Flag: застарілі знання; jcmd є рекомендованим стандартом HotSpot).
  • 🚩 Залишати незахищеним ендпоінт /actuator/heapdump у публічному контурі. (Критична вразливість безпеки: витік облікових записів та даних користувачів).
  • 🚩 Знімати дамп у контейнері в кореневу файлову систему (/tmp), не перевіривши дискові квоти. (Ризик покласти всю ноду Kubernetes через DiskPressure).

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