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

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

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

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
  • Весна / WebMVC 2.5
  • Hibernate 3
  • Кварц 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (и, конечно же, множество других зависимостей, в частности библиотек джакарта-commons)

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

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

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

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

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

То, что я пробовал:

  • Профилирование и фиксация горячих точек.
  • Использование сборщиков мусора STW, Parallel и CMS.
  • Работа с минимальными / максимальными размерами кучи при 1 / 2,2 / 4,4 / 5,6 / 6 приращениях.
  • Работа с пространством перменца с шагом 256M до 1 ГБ.
  • Многие сочетания вышесказанного.
  • Я также консультировался с JVM [настройкой ссылки] (http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html), но не может найти ничего, объясняющего это поведение или любые примеры настройки _which_ параметры для использования в такой ситуации.
  • Я также (безуспешно) попробовал jprofiler в автономном режиме, подключившись к jconsole, visualvm, но я не могу найти ничего, что бы interperet мои данные журнала gc.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

А потом присоединитесь к JConsole, VisualVM ?

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

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

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

День в жизни охотника за утечкой памяти ( release(Classloader) )

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

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

 jmap -permstat  > somefile.txt 

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

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

Я бы посмотрел на непосредственно выделенный ByteBuffer.

Из javadoc.

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

Возможно, код Tomcat использует это для I / O; настройте Tomcat для использования другого разъема.

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

Любой JAXB? Я нахожу, что JAXB – это заполняющее пространство.

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

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

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

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

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

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

Моя технология такова:

грабли 2.2.4

tomcat7

кварцевый плагин 1.0

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

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

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

Кварцевый плагин имел прослушиватель для инициализации и уничтожения сессий спящего режима

 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 nestingCount = new ThreadLocal();

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

До сих пор я просто объясняю сценарий.

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

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

Флеш происходит отлично, но уничтожить нет, потому что HibernatePersistence делает одну проверку перед закрытой сессией спящего режима … В ней рассматривается nestingCount чтобы узнать, является ли значение грубой, чем 1. Если ответ «да», он не закрывает сеанс.

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

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

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

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

Давайте будем гением компьютера.