OutOfMemoryError при горячем развертывании приложения SpringMVC на Tomcat7 - возможная связь с log4j2

У меня возникли проблемы с горячим развертыванием веб-приложения Spring-MVC 4.0 (не SpringBoot). Я пытаюсь обойтись без xml и просто использую JavaConfig. Ошибки OutOfMemoryError возникают при удалении файла web.xml или при развертывании пустого файла web.xml, содержащего только пустой элемент. Это происходит не каждый раз при горячем развертывании приложения, и после успешного горячего развертывания приложение работает правильно, но после трех или четырех горячих развертываний с этой конфигурацией возникает следующая ошибка:

Jul 03, 2015 10:49:43 AM org.springframework.web.context.ContextLoader initWebApplicationContext
    SEVERE: Context initialization failed
    java.lang.OutOfMemoryError: PermGen space
            at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:547)
            at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
            at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
            at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
            at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
            at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
            at org.springframework.context.support.PostProcessorRegistrationDelegate.registerBeanPostProcessors(PostProcessorRegistrationDelegate.java:220)
            at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:615)
            at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:465)
            at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:403)
            at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:306)
            at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106)
            at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:5014)
            at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5524)
            at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
            at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:901)
            at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:877)
            at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:649)
            at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:1081)
            at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1877)
            at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
            at java.util.concurrent.FutureTask.run(FutureTask.java:262)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
            at java.lang.Thread.run(Thread.java:745)

    Jul 03, 2015 10:49:44 AM org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor run
    SEVERE: Unexpected death of background thread ContainerBackgroundProcessor[StandardEngine[Catalina]]
    java.lang.OutOfMemoryError: PermGen space
            at java.util.concurrent.FutureTask.report(FutureTask.java:122)
            at java.util.concurrent.FutureTask.get(FutureTask.java:188)
            at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:816)
            at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:488)
            at org.apache.catalina.startup.HostConfig.check(HostConfig.java:1655)
            at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:328)
            at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:117)
            at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90)
            at org.apache.catalina.core.ContainerBase.backgroundProcess(ContainerBase.java:1374)
            at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1546)
            at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1556)
            at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1524)
            at java.lang.Thread.run(Thread.java:745)

    Exception in thread "ContainerBackgroundProcessor[StandardEngine[Catalina]]" java.lang.OutOfMemoryError: PermGen space
            at java.util.concurrent.FutureTask.report(FutureTask.java:122)
            at java.util.concurrent.FutureTask.get(FutureTask.java:188)
            at org.apache.catalina.startup.HostConfig.deployWARs(HostConfig.java:816)
            at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:488)
            at org.apache.catalina.startup.HostConfig.check(HostConfig.java:1655)
            at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:328)
            at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:117)
            at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90)
            at org.apache.catalina.core.ContainerBase.backgroundProcess(ContainerBase.java:1374)
            at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1546)
            at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1556)
            at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1524)
            at java.lang.Thread.run(Thread.java:745)

Очевидно, что в этой конфигурации каким-то образом происходит утечка памяти.

Может иметь или не иметь значение то, что это веб-приложение использует Log4j2. Ранее Вопрос о переполнении стека исследовал это. Если веб-приложение использует только следующий минимальный файл web.xml

<?xml version="1.0"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
          version="3.0">

    <context-param>
        <param-name>log4jConfiguration</param-name>
        <param-value>file:///path/to/log4j2.xml</param-value>
    </context-param> 

</web-app>

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

Может ли кто-нибудь рискнуть предположить, что здесь может происходить?

Обновление: см. дискуссию между @Makoton и мной ниже. Похоже, что может возникнуть проблема со сборкой мусора, связанная с загрузкой log4j2 из приложения (способ конфигурации Java) и загрузкой из web.xml (традиционный способ). См. эту статью, в которой развенчивается "классическое" предложение о переполнении стека для этой проблемы (аналогично цитируется Макотоном).

Это наводит меня на мысли о SpringBoot, который, насколько я понимаю, загружает Tomcat как часть приложения. Это может быть одним из решений этой проблемы.


person Steve Cohen    schedule 03.07.2015    source источник


Ответы (3)


Я считаю, что Люк прав в том, что эта проблема может быть вызвана ошибкой в ​​Log4j2.

Ошибка заключается в том, что если элемент <display-name> отсутствует в файле web.xml, класс Log4jServletContextListener, отвечающий за управление ресурсами log4j, может только запускать, но не останавливать log4j.

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

Эта ошибка исправлена ​​​​в мастере Git сейчас, и исправление будет частью выпуска log4j 2.5. Между тем, пожалуйста, используйте элемент <display-name> в файле web.xml.

person Remko Popma    schedule 24.10.2015
comment
Вы хотите сказать, что, возможно, ошибка заключается в том, что если элемент ‹display-name› отсутствует в web.xml ИЛИ если нет никакого web.xml? Это была моя ситуация. Я добавил конфигурации log4j2, потому что это был единственный способ начать работу. Похоже, вы говорите, что я могу поместить конфигурацию log4j2 обратно в конфигурацию Java Spring и иметь только это «отображаемое имя» в web.xml. Или дождаться исправления. У меня с этим все в порядке? - person Steve Cohen; 28.10.2015
comment
Если отсутствие web.xml приводит к тому, что log4j не может найти имя контекста сервлета, тогда да. Я не пробовал с Spring. - person Remko Popma; 28.10.2015
comment
Я подтвержу следующие случаи: 1) если отображаемое имя не включено в web.xml, но включен параметр контекста log4jConfiguration, утечки памяти не происходит и не появляются сообщения об ошибках об отсутствующей конфигурации. 2) если включено отображаемое имя и log4jConfiguration c.p. включено, как и предыдущее. 3) если ни один параметр не включен в web.xml, происходят утечки памяти. 4) если отображаемое имя включено в web.xml, а конфигурация log4j отсутствует в web.xml, но программная конфигурация log4j включена в java, то нет утечек памяти, начальное сообщение об ошибке конфигурации отсутствует в журналах, но javaconf делает это правильно. - person Steve Cohen; 28.10.2015
comment
@Remko_Popma Есть ли у разработчиков log4j2 рекомендации по правильному способу настройки log4j2 в java-конфигурации Spring Framework (без web.xml)? - person Steve Cohen; 28.10.2015
comment
@ СтивКоэн, я не знаю. Возможно, вам больше повезет, если вы спросите в списке пользователей log4j. - person Remko Popma; 29.10.2015

Я считаю, что это ошибка в Log4j2.

Я искал подобные утечки памяти PermGen в приложении, над которым я работаю. Используя профилировщик, я мог видеть, что, когда Log4j2 закрывался, он не отменял регистрацию всех MBean-компонентов управления JMX, которые он ранее зарегистрировал. Поскольку эти MBean-компоненты остались во внутреннем реестре MBean-компонентов JVM, классы неразвернутого веб-приложения нельзя было полностью удалить из памяти, поскольку имелись ссылки на экземпляры этих классов.

Как и вы в своем предыдущий вопрос, я тоже видел, как Log4j2 генерирует сообщение

No Log4j context configuration provided. This is very unusual.

во время запуска. В моем случае, однако, исправление было несколько странным — веб-приложение не имело набора <display-name> в нашем файле web.xml, и если дать ему отображаемое имя, это сообщение и утечка памяти исчезли!

Я подозреваю, что в Log4j2 есть ошибка, заключающаяся в том, что он не отменяет регистрацию зарегистрированных MBean-компонентов, если он запускается без контекста, который он упоминает. Передача отображаемого имени — это один из способов создания достаточного контекста, добавление параметра контекста для местоположения конфигурации — другой. Я постараюсь поднять проблему, если ее еще нет.В настоящее время существует проблема в Log4j2 JIRA, в которой упоминаются проблемы, если отображаемое имя отсутствует, теперь я добавил комментарий к этой проблеме, чтобы упомянуть, что в этой ситуации также есть утечка памяти .

Насколько я вижу, у вас есть три варианта.

  1. Работайте с файлом web.xml. Возможно, ему понадобится только <display-name>.

  2. Отключите JMX в Log4j2, установив для системного свойства log4j2.disable.jmx значение true. (Это лучше всего сделать с аргументом командной строки -Dlog4j2.disable.jmx=true.) Этот параметр упоминается в документация Log4j2 JMX.

  3. Загляните в библиотеку Mattias Jiderhamn ClassLoaderLeakPreventor. Одной из его функций во время завершения работы веб-приложения является отмена регистрации любых компонентов MBean, зарегистрированных веб-приложением, но не отмененных. Вы не сможете использовать библиотеку как JAR-файл напрямую, потому что ее нужно добавить в web.xml в качестве прослушивателя, предположительно для тех, кто все еще использует более старые версии сервлетов. Однако есть несколько вариантов обойти это.

    Во-первых, в этой библиотеке есть только один файл .java, и вы, возможно, могли бы включить его исходный код (или только те части его исходного кода, которые отменяют регистрацию MBeans) в своем приложении, если вы также добавите аннотацию @WebListener к классу . Во-вторых, вы можете расширить класс ClassLoaderLeakPreventor и добавить аннотацию @WebListener к вашему подклассу.

person Luke Woodward    schedule 27.09.2015
comment
Спасибо, у меня есть несколько запасных циклов, и я рассмотрю ваши предложения. - person Steve Cohen; 28.10.2015
comment
Я считаю, что проблема JMX в log4j была исправлена, и эта <display-name> является отдельной проблемой. - person Remko Popma; 28.10.2015

Давным-давно у меня такая же проблема с горячим развертыванием tomcat7. Когда я начинаю использовать JAVA_OPTS с

-XX:+CMSClassUnloadingEnabled

решить мою проблему. Я хотел бы объяснить, но я думаю, что это лучшие объяснения.

Что на самом деле делает флаг JVM CMSClassUnloadingEnabled?

http://frankkieviet.blogspot.ca/2006/10/classloader-leaks-dreaded-permgen-space.html

person Makoton    schedule 03.07.2015
comment
Я собираюсь проголосовать за очень информативное предложение, которое побудило меня прочитать о PermGen, но это НЕ решение, потому что оно не решает мою проблему. - Тем не менее, это все еще кажется каким-то образом актуальным: инициализация Log4j2 программно загружает класс-нарушитель, чем бы он ни был, в приложение, а не в контейнер, и это, согласно моей текущей гипотезе, вызывает утечку памяти, которой нет, когда log4j инициализируется механизмом загрузки классов контейнера. - person Steve Cohen; 04.07.2015