Метод обнаружения утечки памяти в больших дампах Java-кучи

Я должен найти утечку памяти в приложении Java. У меня есть некоторый опыт в этом, но я хотел бы получить совет по методологии / стратегии для этого. Любые ссылки и советы приветствуются.

О нашей ситуации:

  1. Кучи дампов больше 1 ГБ
  2. У нас есть куча свалок из 5 случаев.
  3. У нас нет тестов, чтобы спровоцировать это. Это происходит только в (массивной) среде тестирования системы после как минимум недель использования.
  4. Система построена на унаследованной внутренней структуре с таким количеством недостатков дизайна, что их невозможно сосчитать.
  5. Никто не понимает рамки в глубине. Он был передан одному парню в Индии, который едва успевает отвечать на электронные письма.
  6. Со временем мы сделали дампы снимков кучи и пришли к выводу, что с течением времени ни один компонент не увеличивается. Это все, что растет медленно.
  7. Вышесказанное указывает нам на то, что именно доменная система ORM фреймворков увеличивает ее использование без ограничений. (Эта система отображает объекты в файлы?! Так что на самом деле это не ORM)

Вопрос: Какая методология помогла вам добиться успеха в поиске утечек в приложениях масштаба предприятия?

7 ответов

Решение

Это почти невозможно без некоторого понимания основного кода. Если вы понимаете лежащий в основе код, тогда вы можете лучше отсортировать пшеницу от мусора из миллиарда битов информации, которые вы получаете в своих кучах.

Кроме того, вы не можете знать, является ли что-то утечка или нет, не зная, почему класс существует в первую очередь.

Я просто провел последние пару недель, занимаясь именно этим, и использовал итеративный процесс.

Во-первых, я нашел профилировщики кучи в основном бесполезными. Они не могут эффективно анализировать огромные кучи.

Скорее я полагался почти исключительно на гистограммы jmap.

Я думаю, вы знакомы с этим, но для тех, кто не:

jmap -histo:live <pid> > dump.out

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

Я регулярно выбрасывал кучу, каждые 5 минут, 24 часа в сутки. Это может быть слишком гранулированным для вас, но суть та же.

Я провел несколько разных анализов по этим данным.

Я написал скрипт для двух гистограмм и исключения различий между ними. Итак, если java.lang.String было 10 в первом дампе и 15 во втором, мой скрипт выдавал бы "5 java.lang.String", сообщая мне, что он увеличился на 5. Если он упал, число будет отрицательным.

Затем я бы взял некоторые из этих различий, убрал бы все классы, которые переходили от бега к бегу, и взял бы объединение результата. В конце у меня был бы список классов, которые непрерывно росли в течение определенного промежутка времени. Очевидно, это главные кандидаты на утечку классов.

Однако некоторые классы имеют некоторые сохраненные, в то время как другие являются GC'd. Эти классы могут легко идти вверх и вниз в целом, но все же утечка. Таким образом, они могут выпасть из "постоянно растущей" категории классов.

Чтобы найти их, я преобразовал данные во временные ряды и загрузил их в базу данных, в частности, в Postgres. Postgres удобен тем, что предлагает статистические статистические функции, поэтому вы можете выполнять простой линейный регрессионный анализ данных и находить классы с восходящим трендом, даже если они не всегда находятся на вершине диаграмм. Я использовал функцию regr_slope, ища классы с положительным наклоном.

Я нашел этот процесс очень успешным и действительно эффективным. Файлы гистограмм не слишком велики, и их было легко загрузить с хостов. Они не были супер дорогими для запуска в производственной системе (они навязывают большой GC и могут на некоторое время заблокировать виртуальную машину). Я выполнял это в системе с кучей Java 2G.

Теперь все, что можно сделать, - это определить потенциально протекающие классы.

Именно здесь приходит понимание того, как используются классы, и должны ли они быть или не должны быть их.

Например, вы можете обнаружить, что у вас много классов Map.Entry или другого системного класса.

Если вы просто не кешируете String, на самом деле эти системные классы, хотя, возможно, "нарушители", не являются "проблемой". Если вы кешируете какой-то класс приложения, то этот класс является лучшим индикатором вашей проблемы. Если вы не кешируете com.app.yourbean, к нему не будет привязан связанный Map.Entry.

Как только у вас появятся какие-то классы, вы можете начать сканирование базы кода в поисках экземпляров и ссылок. Поскольку у вас есть собственный слой ORM (хорошо это или плохо), вы можете по крайней мере с готовностью взглянуть на его исходный код. Если вы ORM кешируете, это, вероятно, кеширование классов ORM, обертывающих классы вашего приложения.

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

В этом случае вы можете выполнить модульное тестирование, которое затрагивает только 1 (или небольшое количество) вещей, которые, по вашему мнению, могут протекать. Например, вы можете запустить сервер, запустить гистограмму, выполнить одно действие и снова запустить гистограмму. Ваш текущий класс должен был увеличиться на 1 (или какова бы ни была ваша единица работы).

Профилировщик может помочь вам отследить владельцев этого "просочившегося" класса.

Но, в конце концов, вам нужно будет иметь некоторое представление о своей кодовой базе, чтобы лучше понять, что такое утечка, а что нет, и почему объект вообще существует в куче, тем более, почему он может быть сохранен как утечка в твоей куче.

Взгляните на Eclipse Memory Analyzer. Это отличный инструмент (и самодостаточный, не требует установки самого Eclipse), который 1) может очень быстро открывать очень большие кучи и 2) имеет довольно неплохие инструменты автоматического обнаружения. Последнее не идеально, но EMA предоставляет множество действительно хороших способов навигации и запроса объектов в дампе, чтобы найти возможные утечки.

Я использовал это в прошлом, чтобы выследить подозрительные утечки.

Этот ответ распространяется на @Will-Hartung's. Я применил тот же процесс для диагностики одной из моих утечек памяти и подумал, что обмен подробностями сэкономит время других людей.

Идея состоит в том, чтобы посчитать время "графика" в зависимости от использования памяти каждым классом, нарисовать линию, которая суммирует рост, и определить объекты, которые растут быстрее всего:

    ^
    |
s   |  Legend:
i   |  *  - data point
z   |  -- - trend
e   |
(   |
b   |                 *
y   |                     --
t   |                  --
e   |             * --    *
s   |           --
)   |       *--      *
    |     --    *
    |  -- *
   --------------------------------------->
                      time

Преобразуйте свои дампы кучи (нужно несколько) в формат, который удобен для использования postgres из формата дамп кучи:

 num     #instances         #bytes  class name 
----------------------------------------------
   1:       4632416      392305928  [C
   2:       6509258      208296256  java.util.HashMap$Node
   3:       4615599      110774376  java.lang.String
   5:         16856       68812488  [B
   6:        278914       67329632  [Ljava.util.HashMap$Node;
   7:       1297968       62302464  
...

В CSV-файл с указанием даты и времени каждого дампа кучи:

2016.09.20 17:33:40,[C,4632416,392305928
2016.09.20 17:33:40,java.util.HashMap$Node,6509258,208296256
2016.09.20 17:33:40,java.lang.String,4615599,110774376
2016.09.20 17:33:40,[B,16856,68812488
...

Используя этот скрипт:

# Example invocation: convert.heap.hist.to.csv.pl -f heap.2016.09.20.17.33.40.txt -dt "2016.09.20 17:33:40"  >> heap.csv 

 my $file;
 my $dt;
 GetOptions (
     "f=s" => \$file,
     "dt=s" => \$dt
 ) or usage("Error in command line arguments");
 open my $fh, '<', $file or die $!;

my $last=0;
my $lastRotation=0;
 while(not eof($fh)) {
     my $line = <$fh>;
     $line =~ s/\R//g; #remove newlines
     #    1:       4442084      369475664  [C
     my ($instances,$size,$class) = ($line =~ /^\s*\d+:\s+(\d+)\s+(\d+)\s+([\$\[\w\.]+)\s*$/) ;
     if($instances) {
         print "$dt,$class,$instances,$size\n";
     }
 }

 close($fh);

Создать таблицу для размещения данных

CREATE TABLE heap_histogram (
    histwhen timestamp without time zone NOT NULL,
    class character varying NOT NULL,
    instances integer NOT NULL,
    bytes integer NOT NULL
);

Скопируйте данные в новую таблицу

\COPY heap_histogram FROM 'heap.csv'  WITH DELIMITER ',' CSV ;

Запустите запрос slop для запроса размера (num of bytes):

SELECT class, REGR_SLOPE(bytes,extract(epoch from histwhen)) as slope
    FROM public.heap_histogram
    GROUP BY class
    HAVING REGR_SLOPE(bytes,extract(epoch from histwhen)) > 0
    ORDER BY slope DESC
    ;

Интерпретировать результаты:

         class             |        slope         
---------------------------+----------------------
 java.util.ArrayList       |     71.7993806279174
 java.util.HashMap         |     49.0324576155785
 java.lang.String          |     31.7770770326123
 joe.schmoe.BusinessObject |     23.2036817108056
 java.lang.ThreadLocal     |     20.9013528767851

Наклон добавляется в байтах в секунду (поскольку единица измерения эпохи указана в секундах). Если вы используете экземпляры вместо размера, то это количество добавляемых экземпляров в секунду.

Моя одна из строк кода, создающая этот joe.schmoe.BusinessObject была ответственна за утечку памяти. Он создавал объект, добавляя его в массив, не проверяя, существует ли он уже. Другие объекты также были созданы вместе с BusinessObject рядом с утечкой кода.

Можете ли вы ускорить время? то есть можете ли вы написать фиктивный тестовый клиент, который заставляет его выполнять звонки / запросы и т. д. в течение нескольких недель или часов за неделю? Это ваш самый большой друг, и если у вас его нет - напишите его.

Некоторое время назад мы использовали Netbeans для анализа дампов кучи. Это может быть немного медленно, но это было эффективно. Eclipse только что рухнул, и 32-битные инструменты Windows сделали то же самое.

Если у вас есть доступ к 64-битной системе или системе Linux с 3 ГБ или более, вам будет проще анализировать дампы кучи.

У вас есть доступ к журналам изменений и отчетам об инцидентах? Крупные предприятия обычно имеют группы управления изменениями и управления инцидентами, и это может быть полезно для отслеживания, когда начались проблемы.

Когда все пошло не так? Поговорите с людьми и попробуйте получить немного истории. Вы можете попросить кого-нибудь сказать: "Да, именно после того, как они исправили XYZ в патче 6.43, мы получили странные вещи".

У меня был успех с IBM Heap Analyzer. Он предлагает несколько видов кучи, в том числе наибольшее падение размера объекта, наиболее часто встречающиеся объекты и объекты, отсортированные по размеру.

There are great tools like Eclipse MAT and Heap Hero to analyze heap dumps. However, you need to provide these tools with heap dumps captured in the correct format and correct point in time.

This article gives you multiple options to capture heap dumps. However, in my opinion, first 3 are effective options to use and others are good options to be aware. 1. jmap 2. HeapDumpOnOutOfMemoryError 3. jcmd 4. JVisualVM 5. JMX 6. Programmatic Approach 7. IBM Administrative Console

7 Options to capture Java Heap dumps

Если это происходит после недели использования, а ваше приложение настолько византийское, как вы описываете, возможно, вам лучше перезапускать его каждую неделю?

Я знаю, что это не решает проблему, но это может быть эффективным решением. Есть ли временные окна, когда у вас могут быть перебои? Можете ли вы загрузить баланс и перебрать один экземпляр, сохранив второй? Возможно, вы можете запустить перезагрузку, когда потребление памяти превысит определенный предел (возможно, мониторинг через JMX или аналогичный).

Я использовал jhat, это немного грубовато, но это зависит от того, какой у вас был фреймворк.

Другие вопросы по тегам