【问题标题】:Static variables, Tomcat and memory leaks静态变量、Tomcat 和内存泄漏
【发布时间】:2013-11-06 08:22:48
【问题描述】:

我正在调试一个多年来在 Tomcat 应用程序中遇到的问题 - 由于 Webapp 类加载器无法进行 GC,因此在重新启动应用程序时导致内存泄漏。我已经用 JProfiler 拍摄了堆的快照,似乎至少我的一些静态变量没有被释放。

某些类有一个静态最终成员,该成员在首次加载该类时被初始化,因为它是最终的,所以我无法在应用关闭时将其设置为 null。

静态最终变量是 Tomcat 中的反模式,还是我遗漏了什么?我刚刚开始使用 JProfiler 8,所以我可能会误解传入的引用告诉我的内容。

干杯!

卢克

【问题讨论】:

    标签: java tomcat memory-leaks jprofiler


    【解决方案1】:

    这是几年前的事了,但我在 JavaOne 上给出的 presentation 正好涵盖了这个主题。找到泄漏的关键步骤在幻灯片 11 中,但也有许多可能有用的背景信息。

    简短的版本是:

    • 触发泄漏
    • 强制 GC
    • 使用分析器查找具有属性 started=false 的 org.apache.catalina.loader.WebappClassLoader 实例
    • 跟踪该对象的 GC 根 - 这些是您的泄漏

    正如我在演示文稿中指出的那样,找到漏洞是一回事,找到触发它们的原因可能要困难得多。

    我建议在最新的稳定 Tomcat 版本上运行,因为我们一直在改进内存泄漏检测和预防代码,并且生成的警告和错误也可能提供一些指示。

    【讨论】:

    • 我在 7.0.47 上,不幸的是类加载器的 gc 根有几百个。只有大约十几个实例存在,它们都是类的静态变量 - Log4j 1.x 类除外。也许这些就是让类加载器保持活力并因此保持静态的原因?
    • 听起来很有道理。您是否从 ServletContextLister.destroy() 方法调用 LogManager.shutdown() 来清理 log4j?
    • 我是,但仍然有一些 log4j 类存在。我重构了我的应用程序以使用 tomcat 生命周期侦听器来加载 log4j,并将 log4j jar 移动到 $TOMCAT/lib 中,这样 webapp 类加载器就不会加载它。应用程序关闭后剩下的就是我的应用程序,跟踪 GC 根只是链接到静态和类文件。 :(
    • 找到了。 sun.awt.AWTAppContext - 需要确保上下文被 Webapp 类加载器上方的东西抓取。 cdivilly.wordpress.com/2012/04/23/permgen-memory-leak
    • 你需要小心那个 sun.awt.AppContext。从 1.7.0_25 开始的更改意味着调用该方法需要图形环境并启动 AWT 线程。由于这些更改,Tomcat 默认禁用此保护(并且触发泄漏的原始问题已在 1.7.0_02 及更高版本中修复)
    【解决方案2】:

    当类本身被垃圾回收时,静态变量应该被垃圾回收,而当它的类加载器被垃圾回收时也是如此。

    您可以通过引用您的任何类(或您的类的实例)的应用程序类加载器未加载的任何内容轻松创建内存泄漏。查找您没有正确删除的回调侦听器等内容(内部/匿名类很容易被忽略)。

    对您的某个类的单一引用会阻止其类加载器,进而阻止该类加载器加载的任何类被垃圾回收。

    编辑,泄漏阻止所有类 GC 的对象的示例:

    MemoryMXBean mx = ManagementFactory.getMemoryMXBean();
    NotificationListener nl = new NotificationListener() { ... };
    ((NotificationEmitter) mx).addNotificationListener(nl, ..., ...);
    

    如果您使用存在于应用程序范围之外的对象(此处为 MemoryMXBean)注册侦听器(此处为 NotificationListener),则您的侦听器将保持“活动”状态,直到显式删除。由于您的侦听器实例持有对其 ClassLoader(您的应用程序类加载器)的引用,您现在已经创建了一个强大的引用链,阻止了类加载器的 GC,进而阻止了它加载的所有类,以及这些类持有的任何静态变量。

    Edit2:基本上你需要避免这种情况:

    [Root ClassLoader]
           |
           v
          [Application ClassLoader]
                   |
                   v
                   (Type loaded by Root).addSomething()
    

    运行应用程序服务器的 JVM 已经通过根类加载器(可能还有应用程序服务器)加载了 JRE。这意味着这些类永远不会有资格进行 GC,因为其中一些类总是会被实时引用。应用程序服务器将在一个单独的类加载器中加载您的应用程序,当您的应用程序被重新部署(或至少应该)时,它将不再持有对它的引用。但是您的应用程序将与应用程序服务器(至少是 JRE,但通常还有应用程序服务器)共享来自至少 JRE 的所有类。

    在假设情况下,当应用程序服务器要创建一个单独的类加载器(没有父类,实际上是第二个根类加载器)并尝试第二次加载 JRE(作为您的应用程序的私有)时,它会导致很多问题。打算成为单例的类将存在两次,并且两个类层次结构将无法保存另一个的任何引用(由不同的类加载器加载的同一类对 JVM 的不同类型造成)。他们甚至不能使用 java.lang.Object 作为相应“其他”类加载器对象的引用类型。

    【讨论】:

    • 有什么方法可以使用 JProfiler 轻松找到它吗?我查看了保留的引用,尽我所能告诉他们它们只是类中的静态变量。可能的线索是它们是由 ServletContextListener 加载的类,但它本身似乎已被收集。
    • @LukeKolin 我不确定 JProfiler 究竟显示了什么,但原则上,您只需从应用程序外部查找包含对应用程序类的任何引用的类。例如,如果您拥有的所有内容都位于包 com.mydomain 之下,那么任何包含参考 com.mydomain(不是来自 com.mydomain)的内容都是待调查的候选对象。如果幸运的话,保留对象类的名称已经告诉你它在哪里被泄露了。
    • @Durandal 你能举个例子说明“你可以通过让应用程序类加载器没有加载的任何东西引用你的任何类(或你的实例课)。”我只是想知道什么时候可能,如果不是应用程序类加载器,哪个类加载器会加载该类?
    • @MSach 请检查我的编辑,我希望这能让事情更清楚。
    • 谢谢杜兰达尔。我很接近。但是我仍然有一个问题,Agreed MemoryMXBean 对象是远程对象,并且实际上存在于内部保存对 NotificationListener 的引用的其他地方。但我的问题是,当在我的应用程序上运行 GC 时,它不会收集 NotificationListener 引用,因为它不会找到任何进一步的引用该侦听器在应用程序中,它无法搜索远程应用程序上的引用。所以不应该是垃圾收集>
    【解决方案3】:

    这个Blog 可以让您了解应用程序中的内存泄漏。

    【讨论】:

    • 虽然这很有帮助,但我并没有引用外部代码。这只是我的 webapp 中的代码,全部由同一个类加载器加载。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-21
    • 1970-01-01
    • 2012-08-08
    • 2011-02-26
    • 2014-12-25
    • 1970-01-01
    • 2014-12-06
    相关资源
    最近更新 更多