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

Ближе всего я могу воспроизвести проблему, это 32-разрядная машина с более низкими требованиями к памяти. У меня есть контроль. Я исследовал его до смерти с помощью JProfiler и исправил многие проблемы с производительностью (проблемы синхронизации, предварительные компиляции/кэширование запросов xpath, сокращение потока 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 раз в день, и единственное, что я могу показать улов последовательно состоит в том, что сбор мусора действует.

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

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

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

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

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

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

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

Ответ 1

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

Я пробовал 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 регистрировал.

Эти шаги улучшили части производительности 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). Этот класс используется внутри гибернации как основной кэш (я отключил кэши второго уровня и кеши запросов, поддерживаемые EHCache).

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

Из javadoc.

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

grails 2.2.4

tomcat7

quartz-plugin 1.0

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

Еще одна вещь, которую следует учитывать, это то, что quartz-plugin, встраивать сеанс спящего режима в кварцевые потоки, так же, как @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<Integer> nestingCount = new ThreadLocal<Integer>();

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

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

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

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

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

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

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

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

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