6

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

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

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

干杯!

卢克

4

3 回答 3

6

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

简短的版本是:

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

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

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

于 2013-10-28T09:11:50.523 回答
5

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

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

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

编辑,泄漏阻止所有类 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 作为相应“其他”类加载器对象的引用类型。

于 2013-10-27T18:00:18.947 回答
1

博客可以让您了解应用程序中的内存泄漏。

于 2013-10-27T16:00:11.400 回答