Pull to refresh

Анализ утечек PermGen памяти в Java

Reading time 7 min
Views 56K

О чем речь?


Кто занимался веб-разработкой на 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-а. Я не могу гарантировать, что приведенными выше способами можно избавиться от всех видов утечек, однако мне удалось таким образом избавиться от утечек в нескольких крупных проектах.

Ссылки


Tags:
Hubs:
+31
Comments 20
Comments Comments 20

Articles