сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · JVM и сборка мусора

Диагностика утечек памяти в Java

Диагностика: что делаешь, когда упало в три ночи

Процесс с -Xmx128m: в куче занят 1 МиБ, а RSS (resident set size, реально занятая память) - 168 МиБ. Если контейнер прибил такой процесс, снимок кучи не покажет ничего - искать надо там, куда -Xmx не смотрит.

Сеньорская часть JVM-собеса состоит из сценариев, а не определений: «падает с OutOfMemoryError раз в сутки», «латентность выросла втрое», «потоки кончились». Проверяют порядок действий и владение инструментами.

// Формулировки: «как найдёшь утечку памяти?», «что смотришь в thread dump?», «чем профилировать прод?», «куча в норме, а под убивают - что дальше?».

Почерк утечки и подготовка заранее

Мой замер с загрузчиками - готовый образец. Держим по одной ссылке на объект, и число классов в памяти растёт с 722 до 3982, Metaspace с 635 до 5607 КиБ. Отпустили ссылки - 3000 классов выгрузились, Metaspace откатился к 1382 КиБ. Утечка и не-утечка отличались одной живой ссылкой.

На графике кучи это выглядит как пила с растущими минимумами: после каждой полной сборки освобождается всё меньше, дно ползёт вверх, финал - OutOfMemoryError. Ровная пила с постоянным дном - нормальное дыхание сборщика, её трогать не надо.

// Готовиться нужно до инцидента: -XX:+HeapDumpOnOutOfMemoryError и -XX:HeapDumpPath должны стоять заранее. После падения без дампа анализировать нечего, а воспроизвести суточную утечку на стенде получается редко.

heap dump
снимок кучи для анализа того, кто удерживает память
пила с растущими минимумами
почерк утечки: дно после каждой сборки выше предыдущего

Дампы: кучи и потоков

Свёл два потока во взаимную блокировку и спросил JVM через ThreadMXBean. Ответ: найдено 2 потока, дальше поимённо - «заказы-1» в состоянии BLOCKED ждёт монитор (встроенный замок объекта, который берёт synchronized), а держит этот замок «склад-2»; и симметрично наоборот. Такие циклы JVM находит сама, руками сличать стеки не нужно.

Снимок кучи открывают в Eclipse MAT или VisualVM и смотрят два экрана. Dominator tree - кто эксклюзивно удерживает сколько памяти, виновник обычно торчит первой строкой. Path to GC roots - цепочка ссылок от корней сборщика мусора (GC, garbage collector), из-за которой объект не собирается, она и называет виноватого. Если по одному снимку неочевидно, берут два с интервалом в часы и смотрят разницу.

Снимок потоков делают через jstack или jcmd Thread.print, и обязательно СЕРИЕЙ - три-пять штук с интервалом. Одиночный снимок не отличает «зависло» от «медленно движется», различие видно только в динамике. Сотня потоков в BLOCKED на одном мониторе - бутылочное горлышко, весь пул в WAITING - выжран ожиданием внешнего сервиса.

dominator tree
кто эксклюзивно удерживает сколько памяти
thread dump
стеки всех потоков: дедлоки, блокировки, зависания

Когда куча ни при чём

Тот же процесс по шагам: старт - RSS 36 МиБ; сто direct-буферов по мегабайту - RSS 140 МиБ при занятой куче в 1 МиБ; плюс 300 потоков - 168 МиБ. Куча всё это время оставалась практически пустой.

Значит, при OOMKilled от контейнера первым делом выясняют, куча ли это вообще. Сравнивают RSS процесса с занятой кучей и включают NMT (native memory tracking) флагом -XX:NativeMemoryTracking=summary, дальше jcmd VM.native_memory summary разложит потребление по категориям: куча, классы, стеки потоков, скомпилированный код, сам сборщик.

// Нативной зовут память, которую процесс берёт у операционной системы напрямую, мимо кучи: сборщик её не трогает, -Xmx её не считает. Постоянные виновники: direct-буферы, чей лимит по умолчанию равен -Xmx и добавляет столько же сверху, библиотеки на C и тысячи потоков по мегабайту стека каждый. jcmd тут швейцарский нож: GC.heap_info, Thread.print, VM.flags, VM.native_memory, JFR.start.

NMT
native memory tracking: разбивка нативной памяти по категориям
jcmd
универсальная утилита к живой JVM: дампы, флаги, запись

JFR и флеймграфы: смотреть, а не гадать

Прогнал свой счётный цикл дважды: без записи - 6719 мкс на десятом заходе, с включённым JFR в профиле profile - 5710 мкс. Разница утонула в разбросе между заходами, который у меня и так гулял примерно на 400 мкс. Вот и ответ на вопрос «можно ли включать запись в проде».

JFR (Java Flight Recorder) - встроенный самописец JVM: аллокации, паузы сборщика, борьба за локи, операции ввода-вывода. Запись держат постоянно и снимают по факту инцидента - это и есть ответ на «что происходило в три ночи». Смотрят результат в JMC (Java Mission Control).

Профили процессора и аллокаций читают флеймграфом: ширина полосы - доля времени или выделенной памяти, высота - глубина стека. Оптимизация без такого профиля превращается в гадание, а самая широкая полоса регулярно оказывается не в твоём коде, а в ожидании базы.

JFR
штатная запись событий JVM, пригодная для прода
флеймграф
профиль в виде полос: ширина - доля времени или памяти

Как отвечать: «Сервис падает с OutOfMemoryError раз в сутки. Как будешь искать?»

Сначала убеждаюсь, что снимок кучи вообще будет: HeapDumpOnOutOfMemoryError с путём для файла. Если флага не стояло - ставлю и жду следующий цикл, а пока смотрю график кучи: пила с растущими минимумами после полных сборок подтверждает утечку, ровная пила говорит, что дело в другом. Снимок открываю в MAT: dominator tree показывает, кто держит память, обычно виновник в первой строке, а path to GC roots объясняет, почему объект не собирается. Типовые подозреваемые - кэш без выселения, статическая коллекция, неотписанные листенеры, ThreadLocal в пуле потоков. Если по одному снимку неочевидно, беру два с интервалом и смотрю разницу. И обязательно проверяю, куча ли это: у меня был случай, когда в куче занят мегабайт, а RSS процесса 168 мегабайт - тогда течёт нативная память, и это уже NMT, direct-буферы и число потоков.

Почему это сильный ответ: описан полный цикл - подготовка, подтверждение симптома, инструмент, типовые виновники, приём с разницей двух снимков, - плюс развилка на нативную память, о которой забывает большинство.

На чём валят

  • Лечить утечку увеличением -Xmx. Падение придёт позже, а снимок кучи станет только больше и тяжелее для анализа.
  • Не поставить HeapDumpOnOutOfMemoryError заранее. После падения анализировать нечего, а воспроизвести суточную утечку на стенде выходит редко.
  • Один снимок потоков вместо серии. «Зависло» и «медленно движется» различимы только в динамике между снимками.
  • Профилировать на деве с игрушечными данными. Профиль прода другой, и для него есть штатная запись JFR.
  • OOMKilled при нормальной куче списывать на сборщик. Течёт нативная память вне -Xmx: смотри NMT и RSS процесса.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.

  1. #diagnostics_leaks1 / 5
    Чем SoftReference отличается от WeakReference по поведению при GC?
    A)Soft собирается немедленно на первом GC, а weak, наоборот, удерживает объект до самого OutOfMemory
    B)Обе идентичны: разница лишь в названии, оставшемся от разных версий Java по историческим причинам
    C)Soft держится, пока есть память (кэш); weak собирается при первом же GC
    D)Soft и weak запрещают сборку объекта: пока такая ссылка существует, объект остаётся жив
    показать ответ и разбор
    +C)Soft держится, пока есть память (кэш); weak собирается при первом же GC

    // разбор: Сила ссылок: strong (обычная) — объект жив, пока достижим. SoftReference — объект удерживается, ПОКА ХВАТАЕТ ПАМЯТИ; перед выбросом OOM сборщик очищает soft-ссылки — идеально для кэшей «сколько влезет». WeakReference — объект очищается при БЛИЖАЙШЕМ GC, как только на него нет strong-ссылок (основа WeakHashMap, канонизации). PhantomReference — для пост-сборочной очистки через ReferenceQueue (замена finalize). Выбор силы ссылки — способ подсказать GC, насколько объект «нужен».

  2. #diagnostics_leaks2 / 5
    Каким инструментом снимают снимок кучи (heap dump) для поиска утечки?
    A)Jstack — он делает снимок кучи и показывает, какие объекты держат больше всего памяти в приложении
    B)Javac с флагом отладки — он при компиляции встраивает в.class полную карту будущих аллокаций
    C)Обычный System.out.println всех объектов в цикле — простой переносимый способ увидеть кучу
    D)jmap / -XX:+HeapDumpOnOutOfMemoryError, анализ в MAT или VisualVM
    показать ответ и разбор
    +D)jmap / -XX:+HeapDumpOnOutOfMemoryError, анализ в MAT или VisualVM

    // разбор: Heap dump снимают jmap -dump:live,format=b,file=... по PID или автоматически при падении с -XX:+HeapDumpOnOutOfMemoryError; современный способ — jcmd <pid> GC.heap_dump. Анализируют в Eclipse MAT или VisualVM: смотрят гистограмму классов, крупнейших «держателей» (dominators) и цепочки ссылок от GC-корней (кто не даёт собрать объект) — это и вскрывает утечку. Для потоков (deadlock/зависания) — отдельный инструмент jstack (thread dump), а для метрик в реальном времени — jstat/JFR/профайлеры.

  3. #diagnostics_leaks3 / 5
    Что показывает jstack?
    A)Снимок стеков всех потоков (thread dump) — для поиска зависаний/deadlock
    B)Полный снимок кучи со списком всех объектов и занимаемой ими памяти в разбивке по классам
    C)Историю сборок мусора с длительностью каждой паузы Stop-the-World за всё время работы приложения
    D)Скомпилированный JIT-ом машинный код каждого горячего метода для ручного анализа оптимизаций
    показать ответ и разбор
    +A)Снимок стеков всех потоков (thread dump) — для поиска зависаний/deadlock

    // разбор: jstack (или jcmd <pid> Thread.print) делает THREAD DUMP: стеки вызовов и состояния всех потоков в момент снятия. По нему видно, где потоки «висят» (какой метод, какой лок ждут), обнаруживается deadlock (jstack прямо помечает «Found one Java-level deadlock»), выявляются заблокированные пулы. Это первый инструмент при зависании/100% CPU/тормозах. Для памяти нужен heap dump (jmap), для динамики GC/аллокаций — jstat/JFR. Часто снимают несколько thread dump подряд, чтобы увидеть движение.

  4. #diagnostics_leaks4 / 5
    Что означает OutOfMemoryError: unable to create new native thread?
    A)Кончилось место в куче Java под объекты Thread, поэтому новые потоки создать не получится
    B)ОС/лимиты не дают создать ещё поток — слишком много платформенных потоков
    C)Metaspace переполнен классами потоков, и загрузчик не может подгрузить класс нового потока
    D)Сборщик мусора занят и временно запрещает создание потоков до завершения текущего цикла сборки
    показать ответ и разбор
    +B)ОС/лимиты не дают создать ещё поток — слишком много платформенных потоков

    // разбор: Каждый платформенный поток требует нативных ресурсов ОС и памяти под стек (~1МБ). Ошибка «unable to create new native thread» значит, что достигнут ЛИМИТ: число потоков ОС (ulimit -u), исчерпание нативной памяти под стеки, или слишком мало памяти вне кучи. Причина почти всегда — лавина потоков: непулящийся код, растущий/зависший пул, утечка потоков. Лечение: пулы с ограничением, виртуальные потоки для блокирующих задач, аудит числа потоков (jstack/метрики). Куча и -Xmx тут ни при чём — увеличение -Xmx не помогает.

  5. #diagnostics_leaks5 / 5
    Какой инструмент даёт непрерывную низконакладную запись поведения JVM в проде?
    A)System.gc() в цикле — он периодически печатает подробный отчёт о состоянии кучи в стандартный вывод
    B)Javap — он в реальном времени показывает нагрузку на процессор и память работающего приложения
    C)JFR (Java Flight Recorder) — события GC, аллокаций, локов с малым оверхедом
    D)Отладчик в режиме пошагового исполнения, подключённый к продакшену на всё время его работы
    показать ответ и разбор
    +C)JFR (Java Flight Recorder) — события GC, аллокаций, локов с малым оверхедом

    // разбор: Java Flight Recorder (JFR) — встроенный в JDK профайлер, который с ОЧЕНЬ малым оверлеем (единицы процентов) непрерывно пишет события JVM: паузы и циклы GC, аллокации, блокировки, исключения, I/O, hot-методы. Запись (.jfr) анализируют в JDK Mission Control. В отличие от тяжёлых профайлеров или отладчиков, JFR безопасен для ПРОДА и часто держится включённым постоянно — по нему разбирают редкие/плавающие проблемы производительности постфактум. Дополняет jmap/jstack (точечные снимки).

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.