RKDEEP

Обнаружение утечек памяти в java приложении

Kirill Rybkin2021-10-20

Во время работы над проектом столкнулись с тем что память используемая приложением постепенно увеличивается. Приложение написано на java, но утечки памяти случаются.

Основные признаки утечки памяти:

  • Серьезное снижение производительности при длительной непрерывной работе приложения
  • Ошибка кучи OutOfMemoryError в приложении
  • Спонтанные и странные сбои приложения
  • В приложении время от времени заканчиваются объекты подключения

Методология поиска утечек памяти

Для начала необходимо определить, что в приложении есть утечка.

Первое необходимо включить логи сборки мусора для java от 1.4 до 1.8, должны добавить следующие агрументы JVM:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<file-path>

Для Java 9 и выше:

-Xlog:gc*:file=<file-path>

Есть несколько бесплатных инструментов для графического представления GC логов. Рассмотрим вывод GCViewver:

Garbage collector logs

Определяем pid приложения

Это можно сделать либо с помощью стандартной утилиты ps или с помощью утилиты из JDK jps.

dev@inst:/home/dev# jps
14012 ProcessingNetworkData
1236 Kafka
5268 Jps
3926 ProdServerStart
1177 QuorumPeerMain

Определяем объекты доминирующие в памяти.

Для java 8 воспользуемся утилитой jmap из JDK. jmap выведет дамп памяти вашей Java-программы. Увы, работает только с Java 8, если утилита jmap из Java 8 JDK. Утилита считается экспериментальной и в новых JDK не поддерживается. Смотрим статистику по количеству и занимаемой памяти классов

dev@dev:/opt/dev$ sudo jmap -histo 14012

 num     #instances         #bytes  class name
----------------------------------------------
   1:        413819       50815128  [B
   2:        144654        8100624  org.apache.kafka.streams.state.internals.RocksDBRangeIterator
   3:        211985        6783520  java.util.HashMap$Node
   4:        144940        5797600  java.lang.ref.Finalizer
   5:         68322        5353096  [C
   6:        144693        4630176  org.rocksdb.RocksIterator
   7:         93017        4464816  java.util.HashMap
   8:         56104        3141824  java.util.stream.ReferencePipeline$Head
   9:        129373        3104952  org.apache.kafka.common.utils.Bytes
  10:        129373        3104952  org.apache.kafka.streams.KeyValue
  11:         39943        2772248  [Ljava.lang.Object;
  12:         42243        2703552  java.util.stream.ReferencePipeline$2
  13:        144814        2317024  java.util.concurrent.atomic.AtomicBoolean
  14:         37936        2124416  java.util.concurrent.ConcurrentHashMap$KeyIterator
...

Что удерживает ссылки на объекты

Для этого необходимо собрать heapdump. Воспользуемся утилитой jcmd из JDK, формат:

jcmd <pid> GC.heap_dump <file-path>

Как пример наше приложение:

dev@inst:/opt/dev$ jcmd 14012 GC.heap_dump /opt/aqosta/logs/dump.hprof
14012:
Heap dump file created

Для анализа дампа памяти вопользуемся Eclipse Memory Analyzer (MAT)

Eclipse memory analizer report

Смотрим отчет "with outgoing references"

Eclipse inbound objects
Eclipse inbound objects
И отчета видно, что поле openIterators содержит Set, котрый растет в размере и удерживает ссылки на объекты. Это произошло, потому что разработчик не закрыл итератор не вызвал метод .close() и ссылки накаплись. В нашем случае приложение Kafka Stream для обработки потока данных из kafka topic, при не нагруженном приложение эту утечки можно не заметить, а при длительной работе и интенсивности входных данных проблема явно проявляется.