🧠 Раздел 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).

Связанные темы