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