[Из песочницы] Анализ утечек PermGen памяти в Java
О чем речь? Кто занимался веб-разработкой на Java, наверняка сталкивался с такой проблемой как java.lang.OutOfMemoryError: PermGen space. Возникает она, как правило, после перезапуска веб-приложения внутри сервера без перезапуска самого сервера. Перезапуск веб-приложения без перезапуска сервера может понадобиться в процессе разработки, чтобы не ждать лишнее время запуска самого сервера. Если у вас задеплоено несколько веб-приложений, перезапуск всего сервера может быть гораздо дольше перезапуска одного веб-приложения. Или же весь сервер просто нельзя перезапускать, так как другие веб-приложения используются. Первое решение, которое приходит на ум — увеличить максимальный объем PermGen памяти, доступный JVM (сделать это можно опцией -XX: MaxPermSize), но это лишь отсрочит падение, после нескольких перезапусков вы снова получите OutOfMemoryError. Хорошо было бы иметь возможность сколько угодно раз перезапускать и передеплоивать веб-приложение на работающем сервере. О том, как побороть PermGen, и пойдет дальнейший разговор.Что такое PermGen? PermGen — Permanent Generation — область памяти в JVM, предназначенная для хранения описания классов Java и некоторых дополнительных данных. Таким образом, при рестарте веб-приложения все классы загружаются по новой и заполняют PermGen память. Веб-приложение может содержать кучу библиотек, и описания классов могут занимать десятки мегабайт. Кто следит за нововведениями в Java, может быть слышал о том, что в Java 8 отказались от PermGen. Тут можно подумать, что вечную проблему, наконец, исправили, и больше не будет падений от недостатка PermGen памяти. К сожалению это не так, грубо говоря, PermGen теперь просто называется Metaspace, и вы все равно получите OutOfMemoryError.Стоп. А как же сборщик мусора? Всем нам известно, что в Java есть сборщик мусора, который собирает все неиспользуемые объекты. Неиспользуемые классы в PermGen он тоже должен собирать, но только если он правильно настроен, и отсутствуют утечки памяти.Что касается настройки — официальной документации довольно мало, в интернетах есть множество советов использовать различные опции, например -XX:+CMSClassUnloadingEnabled, -XX:+CMSPermGenSweepingEnabled, -XX:+UseConcMarkSweepGC. Я не стал глубоко копать и искать официальную документацию, а методом проб и ошибок определил, что для Java 7 и Tomcat 7 необходимо и достаточно добавить JVM опцию -XX:+UseConcMarkSweepGC. Возможно, вам будет достаточно включить эту опцию, чтобы избавиться от проблем с PermGen. Если нет — то у вас, скорее всего, утечка памяти, что с этим делать — читаем дальше.
Почему происходит утечка PermGen памяти? Для начала пара слов о class loader-ах. Class loader-ы — это объекты в Java, ответственные за загрузку классов. В веб-серверах существует иерархия class loader-ов, на каждое веб-приложение существует по одному class loader-у, плюс несколько общих class loader-ов. Классы внутри веб-приложения загружаются class loader-ом, который соответствует этому веб-приложению. Системные классы и классы, необходимые самому серверу, загружаются общими class loader-ами. Например, как устроена иерархия class loader-ов для Tomcat-а, можно почитать тут.Чтобы сборщик мусора смог собрать все классы веб-приложения, на них не должно быть ссылок вне этого веб-приложения. Теперь вспомним, что каждый объект в Java хранит ссылку на свой класс, т.е. на объект класса java.lang.Class, а каждый класс хранит ссылку на class loader, который загрузил этот класс, а каждый class loader хранит ссылки на все классы, которые он загрузил. Получается, что всего одна ссылка извне на объект веб-приложения тянет за собой все классы веб-приложения и невозможность собрать их сборщиком мусора.
Еще одной причиной утечки может быть поток, который был запущен из веб-приложения, и который не удалось остановить при остановке веб-приложения. Он также хранит ссылку на class loader веб-приложения.
Также популярным вариантом утечки является ThreadLocal переменная, которой присвоен объект из веб-приложения для потока из общего пула. В этом случае поток хранит ссылку на объект. Поток из общего пула не может быть уничтожен, значит объект не может быть уничтожен, значит и весь class loader со всеми классами не может быть уничтожен.
Стандартные средства Tomcat-а К счастью в Tomcat-е существует целый ряд средств для анализа и предотвращения утечек PermGen памяти.Во-первых, в стандартном Tomcat Manager Application есть кнопка «Find leaks» (подробности), которая проанализирует какие веб-приложения оставили после себя мусор после перезапуска.
Но это лишь покажет, какие веб-приложения возможно содержат утечку, толку от этого мало.
Во-вторых, в Tomcat-е есть JreMemoryLeakPreventionListener — решение для общеизвестных возможных вариантов утечек памяти, конфигурируется в server.xml (подробности). Возможно, включение каких-либо опций этого listener-а поможет избавиться от утечек памяти.
И в-третьих самое главное — при остановке веб-приложения Tomcat пишет в лог что именно могло привести к утечке памяти. Например, так:
SEVERE: The web application [/drp] appears to have started a thread named [AWT-Windows] but has failed to stop it. This is very likely to create a memory leak.SEVERE: The web application [/drp] created a ThreadLocal with key of type [org.apache.log4j.helpers.ThreadLocalMap] (value [org.apache.log4j.helpers.ThreadLocalMap@7dc1e95f]) and a value of type [java.util.Hashtable] (value [{session=*2CBFB7}]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
Вот это как раз то, что нам нужно, чтобы продолжить анализ утечек.
И раз уж мы всерьез взялись за дело, надо знать, как правильно проверять очищается у нас PermGen или нет. В этом нам опять же поможет Tomcat Manager Application, который умеет показывать использование памяти, в том числе PermGen.
Еще одна особенность — очистка происходит только после достижения маскимального объема PermGen памяти, так что нужно выставить небольшое значение максимальной доступной PermGen памяти (например, так: -XX: MaxPermSize=100M), чтобы после двух-трех рестартов веб-приложения занятая память достигала 100%, и либо происходила очистка, либо падал OutOfMemoryError если утечки еще остались.
Теперь рассмотрим, как избавиться от утечек на примерах Возьмем следующее сообщение: SEVERE: The web application [/drp] appears to have started a thread named [AWT-Windows] but has failed to stop it. This is very likely to create a memory leak.
Оно говорит нам о том, что веб-приложение запустило и не остановило поток AWT-Windows, следовательно, у него contextClassLoader оказался class loader-ом веб-приложения, и сборщик мусора не может его собрать. Тут мы можем отследить с помощью breakpoint-а с условием по имени потока, кто создал этот поток, и, покопавшись в исходниках, найти, какие есть возможности его остановить, например, проставить какой-то флаг или вызвать какой-то метод, например Thread#interrupt (). Эти действия надо будет выполнить при остановке веб-приложения.
Но еще можно заметить, что название потока похоже на что-то системное… Может JreMemoryLeakPreventionListener, про который мы узнали выше, что-то может сделать с этим потоком? Идем в документацию и видим, что действительно у listener-а есть параметр AWTThreadProtection, который почему-то false по умолчанию. Проставляем его в true в server.xml и убеждаемся, что больше такого сообщения Tomcat не выдает.
В данном случае поток AWT-Windows создавался из-за генерации капчи на сервере с использованием классов работы с изображениями из JDK.
Ок, тут мы отделались простой опцией в Tomcat-е, попробуем что-нибудь посложнее:
SEVERE: The web application [/drp] created a ThreadLocal with key of type [org.apache.log4j.helpers.ThreadLocalMap] (value [org.apache.log4j.helpers.ThreadLocalMap@7dc1e95f]) and a value of type [java.util.Hashtable] (value [{session=*2CBFB7}]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
Тут мы видим, что кто-то положил в ThreadLocal переменную класса ThreadLocalMap некоторое значение и не убрал его. Ищем, где используется класс ThreadLocalMap, находим org.apache.log4j.MDC, а этот класс уже непосредственно используется в нашем веб-приложении для логгирования дополнительной информации. Видим, что вызывается метод put класса MDC, а метод remove не вызывается. Похоже, что вызов remove для каждого put в правильном месте должен помочь. Исправляем, проверяем — работает!
После исправления всех таких ошибок велика вероятность, что вы избавитесь от OutOfMemoryError: PermGen space, по крайней мере, на моей практике это было так.
Анализ с помощью VisualVM Если вы не используете Tomcat, или если исправление ошибок указанных Tomcat-ом в логе не помогло, то можно продолжить анализ с помощью профайлера. Я взял бесплатный профайлер VisualVM входящий в состав JDK.Для начала запустим сервер с одним задеплоенным веб-приложением и перезапустим его, чтобы была видна утечка. Откроем VisualVM, выберем нужный процесс и сделаем heap dump, выбрав соответствующий пункт в выпадающем меню.
Выберем вкладку «OQL Console» и выполним такой запрос: select x from org.apache.catalina.loader.WebappClassLoader x (для других реализаций сервлета класс будет другим).
Один из двух экземпляров остался от первого остановленного веб-приложения, сборщик мусора не смог его собрать. Чтобы определить какой из них является старым — кликаем по одному из них и ищем поле started. У старого started будет false.
В окне «References» показываются все ссылки на этот class loader, нам нужно найти ту, из-за которой сборщик мусора не может его собрать. Для этого щелкаем правой кнопкой мыши по this и выбираем «Show Nearest GC Root».
Отлично, мы нашли какой-то поток, у которого наш старый class loader является contextClassloader-ом. Кликаем по нему правой кнопкой мыши и выбираем «Show Instance».
Смотрим на поля объекта и думаем, за что мы можем зацепиться, чтобы понять, что это за объект, как-то найти код который его создает, поймать в дебаггере, и т.п. В данном случае это имя потока — знакомый нам AWT-Windows. Мы нашли ту же проблему, о которой писал нам Tomcat, только с помощью VisualVM. Как ее решить вы уже знаете.
Итог Мы научились определять, анализировать и исправлять утечки PermGen памяти. Оказалось это не так уж сложно, особенно благодаря встроенным средствам Tomcat-а. Я не могу гарантировать, что приведенными выше способами можно избавиться от всех видов утечек, однако мне удалось таким образом избавиться от утечек в нескольких крупных проектах.Ссылки