将 SpringMVC 应用程序热部署到 Tomcat7 时出现 OutOfMemoryError - 可能与 log4j2 相关

OutOfMemoryError when hot-deploying SpringMVC app to Tomcat7 - possible relation to log4j2

我在热部署 Spring-MVC 4.0(不是 SpringBoot)Web 应用程序时遇到问题。我正在尝试减少 xml-less 并只使用 JavaConfig。 OutOfMemoryErrors 当我删除 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.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)

显然此配置以某种方式泄漏了内存。

此 Web 应用程序使用 Log4j2 可能相关也可能不相关。 对此进行了探索。如果 Web 应用程序仅使用以下最小 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>

然后 WebApp 可以一遍又一遍地热部署而不会出现这些错误。

谁能猜猜这里会发生什么?

更新: - 请参阅下面@Makoton 和我自己之间的讨论。从应用程序(Java 配置方式)加载 log4j2 与从 web.xml(传统方式)加载 log4j2 相比,很可能存在垃圾收集问题。请参阅 this article,它驳斥了针对此问题的 "classic" Stack Overflow 建议(类似于 Makoton 引用的建议)。

这让我想到 SpringBoot,据我所知,它加载 Tomcat 作为应用程序的一部分。这可能是解决此问题的一种方法。

很久以前tomcat7热部署也遇到过同样的问题。当我开始使用 JAVA_OPTS 和

-XX:+CMSClassUnloadingEnabled

解决我的问题。我想解释一下,但我认为这是更好的解释。

What does JVM flag CMSClassUnloadingEnabled actually do?

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

我认为这是 Log4j2 中的错误。

我一直在调查我处理的应用程序中类似的 PermGen 内存泄漏。通过使用探查器,我可以看到当 Log4j2 关闭时,它并没有取消注册它之前注册的所有 JMX 管理 MBean。因为这些 MBean 留在 JVM 的内部 MBean 注册表中,未部署的 Web 应用程序的 classes 无法从内存中完全删除,因为存在对这些 classes 的实例的引用。

就像您在 中一样,我也看到 Log4j2 生成消息

No Log4j context configuration provided. This is very unusual.

在启动期间。然而,就我而言,修复有些奇怪 - Web 应用程序没有在我们的 web.xml 中设置 <display-name>,并且给它一个显示名称使得该消息和内存泄漏消失了!

我怀疑 Log4j2 中存在一个错误,如果它在没有提到的上下文的情况下启动,它不会注销它注册的 MBean。传递显示名称是创建足够上下文的一种方法,为配置位置添加上下文参数似乎是另一种方法。 如果还没有问题,我会考虑提出一个问题。目前 an issue on the Log4j2 JIRA 提到缺少显示名称的问题,我现在已经对此问题添加了评论,提到在这种情况下也存在内存泄漏。

据我所知,你有三个选择。

  1. 使用 web.xml 文件。可能只需要 <display-name>.

  2. 通过将系统 属性 log4j2.disable.jmx 设置为 true 来禁用 Log4j2 中的 JMX。 (这最好使用命令行参数 -Dlog4j2.disable.jmx=true。)此选项在 Log4j2 JMX documentation.

    中提到
  3. 查看 Mattias Jiderhamn's ClassLoaderLeakPreventor 图书馆。它在 Web 应用程序关闭期间的功能之一是注销 Web 应用程序注册但未注销的任何 MBean。您不能直接将库作为 JAR 文件使用,因为它需要作为侦听器添加到 web.xml,大概是为了那些仍在使用旧 Servlet 版本的人的利益。但是,有一些方法可以解决这个问题。

    首先,there is only one .java file in this library,如果您还向 class。其次,您可以扩展 ClassLoaderLeakPreventor class 并将 @WebListener 注释添加到您的 subclass.

我相信卢克是对的,这个问题可能是由a bug in Log4j2引起的。

BUG是如果web.xml中没有<display-name>元素,负责log4j资源管理的Log4jServletContextListenerclass只能启动,不能启动停止 log4j。

这意味着当 Web 应用程序停止或重新启动时不会发生清理。 JMX MBean 没有取消注册,而且任何线程都没有停止,因此 Log4j classes 没有被卸载。每次重新启动 Web 应用程序时,泄漏的内存都会增加,因为每个 Web 应用程序都有自己的 classloader(因此 VM 将它们视为不同的 classes)。

此错误现已在 Git master 中修复,修复将成为 log4j 2.5 版本的一部分。同时,请在 web.xml.

中使用 <display-name> 元素