Отслеживание проблемы утечки памяти / сборки мусора в Java

Это проблема, которую я пытаюсь отследить уже пару месяцев. У меня запущено Java-приложение, которое обрабатывает xml-каналы и сохраняет результат в базе данных. Периодически возникали проблемы с ресурсами, которые очень трудно отследить.

Справочная информация: В производственном ящике (где проблема наиболее заметна) у меня нет особенно хорошего доступа к нему, и я не могу запустить Jprofiler. Это 64-битный четырехъядерный компьютер объемом 8 ГБ, работающий под управлением centos 5.2, tomcat6 и java 1.6.0.11. Все начинается с этих java-оптов

JAVA_OPTS="-server -Xmx5g -Xms4g -Xss256k -XX:MaxPermSize=256m -XX:+PrintGCDetails -
XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+PrintTenuringDistribution -XX:+UseParNewGC"

Стек технологий следующий:

  • Centos 64-bit 5.2
  • Java 6u11
  • Tomcat 6
  • Spring/WebMVC 2.5
  • Hibernate 3
  • Quartz 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (and of course a host of other dependencies, notably the jakarta-commons libraries)

Ближе всего к воспроизведению проблемы я могу подойти к 32-битной машине с меньшими требованиями к памяти. Это я контролирую. Я исследовал это до смерти с помощью JProfiler и исправил многие проблемы с производительностью (проблемы с синхронизацией, предварительная компиляция / кеширование запросов xpath, сокращение пула потоков и удаление ненужной предварительной выборки гибернации, а также чрезмерное «нагревание кеша» во время обработки).

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

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

Проблема кажется в сборке мусора. Мы используем сборщик ConcurrentMarkSweep (как указано выше), потому что исходный сборщик STW вызывал тайм-ауты JDBC и становился все более медленным. Журналы показывают, что по мере увеличения использования памяти это начинает вызывать сбои cms и возвращается к исходному сборщику остановки мира, который затем, кажется, не собирает должным образом.

Однако при работе с jprofiler кнопка «Run GC», похоже, хорошо очищает память, а не показывает увеличивающуюся площадь, но поскольку я не могу подключить jprofiler напрямую к производственной коробке, и разрешение проверенных горячих точек, похоже, не работает, я остался с колдовством настройки слепой сборки мусора.

Что я пробовал:

  • Profiling and fixing hotspots.
  • Using STW, Parallel and CMS garbage collectors.
  • Running with min/max heap sizes at 1/2,2/4,4/5,6/6 increments.
  • Running with permgen space in 256M increments up to 1Gb.
  • Many combinations of the above.
  • I have also consulted the JVM [tuning reference](http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html) , but can't really find anything explaining this behavior or any examples of _which_ tuning parameters to use in a situation like this.
  • I have also (unsuccessfully) tried jprofiler in offline mode, connecting with jconsole, visualvm, but I can't seem to find anything that will interperet my gc log data.

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

Может ли кто-нибудь дать какой-либо совет относительно:
a) Почему JVM использует 8 физических гигабайт и 2 ГБ пространства подкачки, когда он настроен на максимальное значение менее 6.
b) Ссылка на настройку GC, которая на самом деле объясняет или дает разумные примеры того, когда и какие настройки использовать расширенные коллекции.
c) Ссылка на наиболее распространенные утечки памяти Java (я понимаю невостребованные ссылки, но я имею в виду на уровне библиотеки / фреймворка, или что-то более inherenet в структурах данных, как hashmaps).

Спасибо за любую информацию, которую вы можете предоставить.

РЕДАКТИРОВАТЬ
Эмиль Х:
1) Да, мой кластер разработки является зеркалом производственных данных, вплоть до медиа-сервера. Основное различие - это 32/64 бит и объем доступной оперативной памяти, который я не могу легко воспроизвести, но код, запросы и настройки идентичны.

2) Есть некоторый унаследованный код, который полагается на JaxB, но при переупорядочении заданий, чтобы попытаться избежать конфликтов планирования, это выполнение обычно исключается, поскольку оно выполняется один раз в день. Основной синтаксический анализатор использует запросы XPath, которые вызывают пакет java.xml.xpath. Это было источником нескольких горячих точек, для одного запросы не были предварительно скомпилированы, а для двух ссылки на них были в жестко запрограммированных строках. Я создал потокобезопасный кеш (hashmap) и сделал так, чтобы ссылки на запросы xpath были окончательными статическими строками, что значительно снизило потребление ресурсов. Запросы по-прежнему составляют значительную часть обработки, но это должно быть так, потому что это основная ответственность приложения.

3) Дополнительное примечание, другим основным потребителем являются операции с изображениями из JAI (повторная обработка изображений из канала). Я не знаком с графическими библиотеками java, но из того, что я обнаружил, они не особо протекают.

(спасибо за ответы, ребята!)

ОБНОВЛЕНИЕ:
Мне удалось подключиться к производственному экземпляру с помощью VisualVM, но он отключил параметр «Визуализация сборщика мусора / запуск-сборщик мусора» (хотя я мог просматривать его локально). Интересная вещь: распределение кучи виртуальной машины подчиняется JAVA_OPTS, а фактическая выделенная куча удобно расположена на 1-1,5 гигабайт и, похоже, не протекает, но мониторинг уровня коробки по-прежнему показывает образец утечки, но это не отражается в мониторинге ВМ. На этом ящике больше ничего не работает, так что я в тупике.


person liam    schedule 01.07.2009    source источник
comment
Используете ли вы для тестирования данные реального мира и базу данных реального мира? Желательно копию производственных данных?   -  person Emil H    schedule 02.07.2009
comment
+1 - это один из лучших вопросов, которые я когда-либо читал. Хотел бы я предложить больше помощи. Я вернусь к этому, чтобы посмотреть, есть ли у кого-нибудь что сказать умное.   -  person duffymo    schedule 02.07.2009
comment
А какой XML-парсер вы используете?   -  person Emil H    schedule 02.07.2009
comment
Вы смотрели количество выделенных ByteBuffers и кто их распределяет?   -  person Sean McCauliff    schedule 10.07.2009
comment
Проверьте этот ответ: stackoverflow.com/a/35610063, он содержит подробную информацию об утечках собственной памяти Java.   -  person Lari Hotari    schedule 13.09.2019


Ответы (7)


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

Я пробовал jmap, пока процесс работал, но обычно это приводило к дальнейшему зависанию jvm, и мне приходилось запускать его с --force. Это приводило к дампам кучи, в которых, казалось, не хватало большого количества данных или, по крайней мере, отсутствовали ссылки между ними. Для анализа я попробовал jhat, который представляет много данных, но не так много о том, как их интерпретировать. Во-вторых, я попробовал инструмент анализа памяти на основе eclipse (http://www.eclipse.org/mat/), который показал, что куча - это в основном классы, связанные с tomcat.

Проблема заключалась в том, что jmap не сообщал о фактическом состоянии приложения и улавливал только классы при завершении работы, которые в основном были классами tomcat.

Я попробовал еще несколько раз и заметил, что было очень много объектов модели (на самом деле в 2-3 раза больше, чем было помечено как общедоступные в базе данных).

Используя это, я проанализировал журналы медленных запросов и несколько не связанных с производительностью проблем. Я пробовал выполнять сверхленивую загрузку (http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html), а также замену нескольких операций гибернации прямыми запросами jdbc (в основном там, где речь идет о загрузке и работе с большими коллекциями - замены jdbc просто работали непосредственно с таблицами соединения) и заменили некоторые другие неэффективные запросы, которые mysql регистрировал.

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

Наконец, я нашел вариант: -XX: + HeapDumpOnOutOfMemoryError. В итоге получился очень большой (~ 6,5 ГБ) файл hprof, который точно отображал состояние приложения. По иронии судьбы, файл был настолько большим, что jhat не мог его проанализировать даже на коробке с 16 ГБ оперативной памяти. К счастью, MAT смогла построить несколько красивых графиков и показать некоторые более точные данные.

На этот раз бросился в глаза единственный кварцевый поток, занимавший 4,5 ГБ из 6 ГБ кучи, и большая часть из них была StatefulPersistenceContext в спящем режиме (https://www.hibernate.org/hib_docs/v3/api/org/hibernate/engine/StatefulPersistenceContext.html). Этот класс используется внутри спящего режима в качестве основного кеша (я отключил кеши второго уровня и кеши запросов, поддерживаемые EHCache).

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

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

Проблема в том, что пул потоков никогда не освобождает сеанс, поэтому спящий режим оставался резидентным и поддерживал кеш для жизненного цикла сеанса. Поскольку здесь использовалась поддержка шаблонов спящего режима пружин, явного использования сеансов не было (мы используем иерархию dao -> manager -> driver -> quartz-job, dao вводится с конфигурациями гибернации через spring, поэтому операции делается прямо по шаблонам).

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

Решение: создать метод dao, который явно вызывает session.flush () и session.clear (), и вызывать этот метод в начале каждого задания.

Приложение работает уже несколько дней без проблем с мониторингом, ошибок памяти или перезапусков.

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

person liam    schedule 04.12.2009
comment
Хорошее краткое изложение вашего процесса отладки и спасибо за отслеживание и публикацию решения. - person Boris Terzic; 06.12.2009
comment
Спасибо за хорошее объяснение. У меня была аналогичная проблема в сценарии пакетного чтения (SELECT), в результате которого StatefulPersistenceContext стал настолько большим. Я не мог запустить em.clear () или em.flush (), так как в моем основном методе цикла было @Transactional(propagation = Propagation.NOT_SUPPORTED). Это было решено путем изменения распространения на Propagation.REQUIRED и вызова em.flush / em.clear (). - person Mohsen; 18.11.2011
comment
Одна вещь, которую я не понимаю: если сеанс никогда не сбрасывался, это означает, что фактические данные не были сохранены в БД. Разве эти данные не извлекаются где-то еще в вашем приложении, чтобы вы могли видеть, что они отсутствуют? - person yair; 02.04.2013
comment
Предоставленная ссылка на StatefulPersistenceContext не работает. Это docs.jboss.org/ hibernate / orm / 4.3 / javadocs / org / hibernate / engine / прямо сейчас? - person Victor Stafusa; 05.04.2013
comment
Лиам, спасибо большое. У меня такая же проблема, и MAT указывает на спящий режим statefulPersistentContext. Думаю, прочитав вашу статью, я получил достаточно намеков. Спасибо за такую ​​замечательную информацию. - person Reddymails; 24.07.2014
comment
Спасибо, у нас была такая же проблема, и вызов em.flush () и em.clear () исправил ее. - person iko_wp; 03.01.2018

Можете ли вы запустить производственную коробку с включенным JMX?

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=<port>
...

Мониторинг и управление с помощью JMX

А затем прикрепить с помощью JConsole, VisualVM?

Можно ли делать дамп кучи с помощью jmap ?

Если да, вы можете проанализировать дамп кучи на наличие утечек с помощью JProfiler (у вас уже есть), jhat, VisualVM, Eclipse MAT. Также сравните дампы кучи, которые могут помочь найти утечки / шаблоны.

И, как вы упомянули, jakarta-commons. При использовании jakarta-commons-logging возникает проблема, связанная с удержанием загрузчика классов. Для хорошего прочтения этого чека

Один день из жизни охотника за утечками памяти ( _ 2_ < / а>)

person jitter    schedule 01.07.2009
comment
1) Сегодня я действительно пробовал visualvm и несколько других инструментов, но мне нужно правильно открыть порты. 2) Я видел проблему c-logging на моей последней работе, и эта проблема напомнила мне об этом. В общекорпоративном сервисе регулярно происходили сбои, и это было связано с известной утечкой в ​​сообществе, я считаю, что это было что-то похожее на то, что вы связали. Я попытался сохранить большую часть журнала как log4j, но у меня нет особого выбора для зависимых проектов, для которых требуется пакет commons. У нас также есть несколько классов, использующих simpleFacade, сейчас я ищу, смогу ли я сделать вещи более последовательными. - person liam; 02.07.2009

Похоже, утечка памяти, отличной от кучи, вы упоминаете, что куча остается стабильной. Классическим кандидатом является permgen (постоянная генерация), которая состоит из двух вещей: загруженных объектов класса и интернированных строк. Поскольку вы сообщаете о подключении к VisualVM, вы должны увидеть количество загруженных классов, если количество загруженных классов продолжает расти (важно, visualvm также показывает общее количество когда-либо загруженных классов, Ничего страшного, если он возрастет, но количество загруженных классов должно стабилизироваться через определенное время).

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

jmap -permstat <pid> > somefile<timestamp>.txt

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

После того, как вы определили определенные классы как загружаемые, а не выгружаемые, вы можете мысленно выяснить, где они могут быть сгенерированы, в противном случае вы можете использовать jhat для анализа дампов, созданных с помощью jmap -dump. Я сохраню это для будущего обновления, если вам понадобится информация.

person Boris Terzic    schedule 06.07.2009
comment
Хорошее предложение. Я попробую это сегодня днем. - person liam; 06.07.2009
comment
jmap не помог, но был близок. см. полный ответ для объяснения. - person liam; 04.12.2009

Я бы поискал напрямую выделенный ByteBuffer.

Из файла javadoc.

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

Возможно, код Tomcat использует это do для ввода-вывода; настроить Tomcat для использования другого коннектора.

В противном случае у вас может быть поток, который периодически выполняет System.gc (). "-XX: + ExplicitGCInvokesConcurrent" может быть интересным вариантом.

person Sean McCauliff    schedule 01.07.2009
comment
1) Когда вы говорите коннектор, вы имеете в виду коннектор БД или другой класс, связанный с вводом-выводом? Лично я бы предпочел не прилагать усилий для введения нового пула соединений, даже если c3p0 близок к этому, но я бы не стал использовать его как возможность. 2) Явного флага GC не встречал, но обязательно учту. Однако это кажется немного хакерским, и с устаревшей кодовой базой такого размера я пытаюсь отойти от этого подхода. (например: несколько месяцев назад мне пришлось отследить несколько точек, которые просто порождали потоки в качестве побочных эффектов. Теперь потоки объединены). - person liam; 02.07.2009
comment
1) Прошло некоторое время с тех пор, как я настроил tomcat. У него действительно была концепция, называемая коннектором, поэтому вы могли настроить его для прослушивания запросов от Apache httpd или прямого прослушивания HTTP. В какой-то момент существовал HTTP-коннектор NIO и базовый HTTP-коннектор. Вы можете увидеть, какие параметры конфигурации доступны для HTTP-коннектора NIO или узнать, доступен ли единственный базовый коннектор. 2) Вам нужен только поток, который периодически вызывает System.gc (), или вы можете повторно использовать временной поток. Да, это совершенно хакерское занятие. - person Sean McCauliff; 03.07.2009
comment
См. stackoverflow.com / questions / 26041117 / для устранения утечек собственной памяти. - person Lari Hotari; 25.08.2016

Любой JAXB? Я считаю, что JAXB - это средство для перманентного пространства.

Кроме того, я считаю, что visualgc, теперь поставляемый с JDK 6, является отличным способом увидеть что происходит в памяти. Он прекрасно показывает райское пространство, пространство поколений и перманентное поведение, а также переходное поведение GC. Все, что вам нужно, это PID процесса. Может быть, это поможет, пока вы работаете над JProfile.

А как насчет аспектов трассировки / ведения журнала Spring? Может быть, вы можете написать простой аспект, применить его декларативно и таким образом сделать профилировщик для бедняков.

person duffymo    schedule 01.07.2009
comment
1) Я работаю с SA, чтобы попытаться открыть удаленный порт, и я собираюсь попробовать собственные инструменты на основе java / jmx (я пробовал несколько, включая jprofiler - отличный инструмент! - но было слишком сложно получить там есть соответствующие библиотеки системного уровня). 2) Я очень настороженно отношусь к аспектно-ориентированному чему-либо, даже с весны. По моему опыту, даже наличие зависимости от этого делает вещи более запутанными и сложными в настройке. Я буду иметь это в виду, если ничего не работает. - person liam; 02.07.2009

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

Похоже, это связано с вариантом использования, который выполняется до 40 раз в день, а затем уже не в течение нескольких дней. Надеюсь, вы отслеживаете не только симптомы. Это должно быть что-то, что вы можете сузить, отслеживая действия субъектов приложения (пользователей, вакансий, сервисов).

Если это происходит при импорте XML, вам следует сравнить XML-данные за 40 дней сбоев с данными, которые были импортированы в день без сбоев. Может быть, это какая-то логическая проблема, которую вы не найдете только в своем коде.

person cafebabe    schedule 02.07.2009

У меня была такая же проблема, но с некоторыми отличиями ..

Моя технология следующая:

grails 2.2.4

tomcat7

quartz-plugin 1.0

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

Еще одна вещь, которую следует учитывать, - это плагин quartz, вводящий сеанс гибернации в кварцевые потоки, как говорит @liam, и кварцевые потоки все еще живы, пока я не закончу приложение.

Моя проблема заключалась в ошибке ORM grails в сочетании с тем, как плагин обрабатывает сеанс и два моих источника данных.

В плагине Quartz есть слушатель для инициализации и уничтожения сеансов гибернации.

public class SessionBinderJobListener extends JobListenerSupport {

    public static final String NAME = "sessionBinderListener";

    private PersistenceContextInterceptor persistenceInterceptor;

    public String getName() {
        return NAME;
    }

    public PersistenceContextInterceptor getPersistenceInterceptor() {
        return persistenceInterceptor;
    }

    public void setPersistenceInterceptor(PersistenceContextInterceptor persistenceInterceptor) {
        this.persistenceInterceptor = persistenceInterceptor;
    }

    public void jobToBeExecuted(JobExecutionContext context) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.init();
        }
    }

    public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.flush();
            persistenceInterceptor.destroy();
        }
    }
}

В моем случае persistenceInterceptor экземпляров AggregatePersistenceContextInterceptor, и у него был список HibernatePersistenceContextInterceptor. По одному для каждого источника данных.

Каждая операция, выполняемая с AggregatePersistenceContextInterceptor, передается в HibernatePersistence без каких-либо изменений или обработок.

Когда мы вызываем init() на HibernatePersistenceContextInterceptor, он увеличивает статическую переменную ниже

private static ThreadLocal<Integer> nestingCount = new ThreadLocal<Integer>();

Я не знаю, зачем нужен этот статический счет. Я просто знаю, что он увеличился в два раза, по одному на источник данных, из-за реализации AggregatePersistence.

Пока здесь я просто объясняю cenario.

Проблема возникает сейчас ...

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

Сброс происходит отлично, а уничтожения нет, потому что HibernatePersistence, выполняет одну проверку перед закрытием сеанса гибернации ... Он проверяет nestingCount, является ли значение больше, чем 1. Если ответ положительный, он не закрывает сеанс.

Упрощение того, что сделал Hibernate:

if(--nestingCount.getValue() > 0)
    do nothing;
else
    close the session;

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

Чтобы решить эту проблему, я настраиваю слушателя для вызова clear перед уничтожением и дважды вызываю destroy (по одному для каждого источника данных). Обеспечение того, чтобы мой сеанс был чистым и уничтоженным, и, если уничтожение не удалось, по крайней мере, он был чист.

person jpozorio    schedule 12.05.2016