我最近开始分析我正在使用 VisualVM 编写的 osgi java 应用程序。我注意到的一件事是,当应用程序开始(通过 JMS)向客户端发送数据时,加载的类的数量开始以稳定的速度增加。但是,堆大小和 PermGen 大小保持不变。即使停止发送数据,类的数量也不会下降。这是内存泄漏吗?我认为是这样,因为加载的类必须存储在某个地方,但是即使在我运行应用程序几个小时后,堆和 permgen 也不会增加。
有关我的分析应用程序的屏幕截图,请转到此处
您是否以某种方式动态创建新类?
谢谢你的帮助。我弄清楚了问题所在。在我的一个课程中,我使用 Jaxb 创建了一个 XML 字符串。在这样做时,JAXB 使用反射来创建一个新类。
JAXBContext context = JAXBContext.newInstance(this.getClass());
因此,尽管 JAXBContext 没有在堆中说明,但类已经加载。
我再次运行我的程序,我看到了一个正常的高原,正如我所期望的那样。
您可能会发现一些热点标志可用于理解此行为,例如:
这是一个很好的参考:
http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp
我敢打赌,您的问题与字节码生成有关。
许多库使用 CGLib、BCEL、Javasist 或 Janino 在运行时为新类生成字节码,然后从受控类加载器加载它们。释放这些类的唯一方法是释放对类加载器的所有引用。
由于类加载器由每个类持有,这也意味着您不应该释放对所有类的引用 [1]。你可以用一个不错的分析器来捕捉这些(我使用 Yourkit - 搜索具有相同保留大小的多个类加载器实例)
一个问题是 JVM 默认不卸载类(原因是向后兼容性——人们(错误地)假设静态初始化程序只会执行一次。事实是每次加载类时它们都会执行。)启用卸载,您应该通过一些使用以下选项:
-XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
(使用 JDK 1.5 测试)
即便如此,过多的字节码生成也不是一个好主意,所以我建议你在你的代码中查找罪魁祸首并缓存生成的类。常见的违规者是脚本语言、动态代理(包括由应用程序服务器生成的代理)或巨大的 Hibernate 模型(在这种情况下,您可以增加 permgen)。
也可以看看:
除非我误解了,否则我们在这里查看的是加载的类,而不是实例。
当您的代码第一次引用一个类时,JVM 让 ClassLoader 出去并从 .class 文件或类似文件中获取有关该类的信息。
我不确定它会在什么条件下卸载一个类。当然,它不应该卸载任何带有静态信息的类。
因此,我希望有一个与您的模式大致相似的模式,当您的应用程序运行时,它会进入区域并引用新类,因此加载的类的数量会越来越多。
但是,有两件事对我来说似乎很奇怪:
我预计它会呈上升趋势,但在一条不稳定的线中,然后随着 JVM 已经加载您的程序引用的大部分类而逐渐减少。我的意思是,在大多数应用程序中引用的类数量是有限的。
您是否以某种方式动态创建新类?
我建议通过相同的调试器运行一个更简单的测试应用程序以获得基线案例。然后你可以考虑实现你自己的 ClassLoader 吐出一些调试信息,或者也许有一个工具可以让它报告。
您需要弄清楚这些正在加载的类是什么。
是的,这通常是内存泄漏(因为我们并没有真正直接处理内存,它更像是类实例泄漏)。我之前已经经历过这个过程,通常它是一些添加到旧工具包中的侦听器,它不会自行删除。
在旧代码中,侦听器关系会导致“侦听器”对象保留。我会查看较旧的工具包或未经过多次修订的工具包。在更高版本的 JDK 上运行的任何长期存在的库都会知道引用对象,这消除了对“删除侦听器”的要求。
此外,如果您每次都重新创建它们,请在 Windows 上调用 dispose。如果您不这样做,我认为它们永远不会消失(实际上也有关闭设置的处置)。
不要担心 Swing 或 JDK 侦听器,它们都应该使用引用,所以你应该没问题。
使用Eclipse Memory Analyzer检查重复的类和内存泄漏。同一个类可能会被多次加载。
问候, 马库斯