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

Ссылки


Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 20

    0
    К сожалению это не так, грубо говоря, PermGen теперь просто называется Metaspace, и вы все равно получите OutOfMemoryError

    Это в том случае, если вы его ограничиваете параметром командной строки, верно? Если не ограничиваете, то описания классов могут занимать хоть всю доступную приложению память. Или это не так?
      +1
      Я не смотрел подробно как работает Metaspace в Java 8. Но даже если есть возможность дать всю доступную память для описания классов, утечка есть утечка. Рано или поздно она скушает всю доступную память. Другое дело, что в каком-то конкретном случае утечка может быть небольшая, OutOfMemoryError может случиться только после очень большого количества передеплоев, и тут надо решать, что целесообразнее: устранять утечку, или жить так.
      +3
      >Я не стал глубоко копать и искать официальную документацию, а методом проб и ошибок определил, что для Java 7 и Tomcat 7 необходимо и достаточно добавить JVM опцию -XX:+UseConcMarkSweepGC

      Довольно спорный совет. Включив это опцию вы меняете сборщик мусора на Concurrent Mark Sweep, а это может значительно повлиять на работу приложения. При этом возможность выгружать классы во время работы приложения вы получаете просто как бонус.
      Если говорить о других сборщиках мусора, то там обычно сборка мусора в PermGen выполняется при Full GC.
        0
        Согласен с вами, что менять алгоритм GC просто потому что так заработало — спорно. В свое оправдание могу сказать, что CMS вроде бы является рекомендуемым для продакшна (пруф1, пруф2). Ну и если вы дадите ссылку на документацию относительно сборки мусора в PermGen с разными алгоритмами сборки — буду очень благодарен.
          0
          Вторая ссылка должна быть такой
            0
            Первая ссылка:
            The CMS garbage collector is the first and most-widely used low-latency collector

            Ключевое слово здесь low-latency. Используя CMS вы улучшаете latency, но ухудшаете throughput. Грубо говоря, вы жертвуете производительностью, зато избегаете длинных GC пауз.

            По второй ссылке — вы бы еще древнее что-нибудь нашли! Статья времен WebLogic Server 9.2, 2006 г. Да и что-то там c настройками перемудрили — XX:SurvivorRatio=128 — это что-же survivor space занимает 32/130MB ~ 250KB?

            По сути, вы можете выполнить прекрасную команду в консоли
            java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version | grep Unloading
            Она покажет вам все опции, влияющие на unloading классов. По дефолту ClassUnloading включена, т.е. по факту классы в permgen будет собираться. А вот как раз если вы используете CMS GC, то для unloading'а классов вам нужно включить -XX:+CMSClassUnloadingEnabled.

            А вообще по теме GC рекомендую отличный доклад от Владимира Иванова , а на недавней конференции Joker как раз был доклад про утечки памяти.
              0
              Насколько я знаю -XX:CMSClassUnloadingEnabled включена по дефолту, при использовании CMS (на не очень древних версиях Hotspot).
                0
                На моей машине

                jdk1.7.0_45
                bool CMSClassUnloadingEnabled = false {product}

                jdk1.8
                bool CMSClassUnloadingEnabled = true {product}
                  0
                  Ну, jdk1.8 — не очень древняя версия :) На самом деле посмотрел только на дату resolved и не обратил внимание, что fixed версия 1.8 bugs.java.com/bugdatabase/view_bug.do?bug_id=8006481
                0
                Посмотрел как ведут себя разные сборщики мусора с PermGen-ом:
                Parallel (дефолтный на моей машине) — PermGen не очищается, падает OutOfMemoryError
                Serial — PermGen очищается после достижения 100%
                Concurrent Mark Sweep — PermGen очищается после достижения 100%

                Вызвать очистку PermGen вручную мне не удалось, на System.gc() не реагирует.

                При этом
                bool CMSClassUnloadingEnabled = false {product}
                bool ClassUnloading = true {product}

                Все-таки категорически не хватает документации.

                JDK 1.7.0_55-b13, Windows7, amd64, Intel Core i5 650 @ 3.20GHz
                  0
                  Тесты в студию!

                  Собственно, вот мои тесты: gist.github.com/SerCeMan/876fcdfaca602af2df8d

                  Судя по тесту, классы выгружаются при любом GC, даже если мы опция CMSClassUnloadingEnabled стоит false.

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

                    С вашим тестом классы действительно выгружаются с любым gc.
                    Кстати, вот более простой вариант теста gist.github.com/zgmnkv/0e7ac1fe91c7d51d41a3
                    Попутно более-менее прояснил значение опций:
                    ClassUnloading — если true, то классы выгружаются; если false, то не выгружаются, и падает OutOfMemoryError, логично в общем то.
                    CMSClassUnloadingEnabled — влияет только на CMS GC; если true, то классы выгружаются чаще, PermGen не доростает до максимума.
          +2
          Совсем недавно на CodeFreeze была встреча на эту тему. Только внимание было уделено Eclipse Memory Analyzer.
            –2
            > Также популярным вариантом утечки является ThreadLocal переменная, которой присвоен объект из веб-приложения для потока из общего пула. В этом случае поток хранит ссылку на объект.

            Это совершенно неверно. Поток нигде не хранит ссылку на объект. Внутри ThreadLocal использует структуру похожую на Map для сопоставления объекта с потоком. Сборщик мусора соберет ваш ThreadLocal и все его объекты также как обычный HashMap. При этом потоки естественно останутся нетронутыми.
              0
              Почему вы так решили, даже не заглянув в исходники? ThreadLocal внутри себя хранит ровно одно нестатическое поле — это int threadLocalHashCode. Сами данные лежат в Thread.threadLocals. Структура ThreadLocalMap действительно похожа на HashMap (точнее это мультимэп), вот только ключи — это не потоки, а идентификаторы переменных. Автор поста как раз прав.
                0
                По факту там используются слабые ссылки на сами ThreadLocal, но это не решает всей проблемы: объект ThreadLocalMap.Entry хранит также сильную ссылку на значение. Поэтому даже если ThreadLocal протух, и Entry теперь ни на что не ссылается, сам Entry существует, пока ThreadLocalMap явно не освободит этот слот, что может произойти после нескольких обращений к другим ThreadLocal-переменным в том же треде. Если этого никто делать не будет, значение из исходного ThreadLocal (и всё, что из него растёт) останутся сильно связанными.
                  0
                  Сорри, действительно. Entry у ThreadLocalMap — это WeakReference на ThreadLocal, так что ThreadLocal все-таки собирается GC. Странность реализации заключается в том, что Entry и value продолжает храниться в таблице, пока Thread не вызовет не set() для любой TheadLocal.
                    0
                    Собственно, вся сила ThreadLocal как раз в том, что они не требуют конкурентного доступа: никаких проблем при одновременной записи из десяти тредов, данные полностью разделены. Реализация вида ConcurrentHashMap<Thread, Object> была бы во много раз медленнее из-за рехэшинга разделённого объекта, возможности попасть двум тредам в одну корзину и так далее.
                0
                Это каждый раз надо делать?
                В общем как человек который уже много лет пишет на яве могу сказать что я не использую несколько приложений на одном tomcat
                У нас работает несколько экземпляров jetty одного и того же приложения чтобы распределять нагрузку. Запросы распределяет nginx.
                Мне казалось так все делают.
                  0
                  Что вы имеете ввиду под «каждый раз»? При разработке нового приложения?
                  Это дело каждого, если вам мешают такие ошибки — исправляйте их, если не мешают — не исправляйте. Если приложение написано правильно, завершает все созданные потоки, убирает проставленные ThreadLocal, не проставляет свои ссылки в системные объекты, то у вас не должно возникать подобных утечек.
                  По поводу распределения нагрузки — это из другой оперы. Я имел ввиду, что разные приложения или разные части одного приложения задеплоены в одном tomcat-е.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое