我们有一个在 Glassfish V2.1.1 下运行的复杂应用程序。为了能够动态加载我们的代码,我们实现了一个能够重新定义类的 CustomClassloader。行为非常简单:当动态加载的类发生变化时,CustomClassloader 的当前实例被“删除”并创建一个新实例以重新定义所需的类。
这很好用,除了在重新加载同一个类几次之后(因此每次创建新的 CustomClassloader 时),我们都会收到 PermGen 空间错误,因为 CustomClassloader 的其他实例没有被垃圾收集。(这个类应该只有一个实例)
我尝试了不同的方法来追踪泄漏的位置:
- visualvm => 我做了一个堆转储并提取 CustomClassloader 的所有实例。我可以看到它们都没有最终确定。当我检查最近的 GC 根时,visualvm 告诉我没有(最后一个实例除外,因为它是“真正的”使用过的)。
- jmap/jhat => 它给了我几乎相同的结果:我看到了 CustomClassloader 的所有实例,然后当我单击链接以查看其中一个的引用在哪里时,我得到一个空白页,这意味着没有.. .
- Eclipse Memory Analyzer Tool => 当我运行以下 OQL 查询时得到一个奇怪的结果:
SELECT c FROM INSTANCEOF my.package.CustomClassloader c
只有一个结果,表明只有一个实例,这显然是不正确的。
我还检查了此链接并在创建新的 CustomClassloader 时实现了一些资源释放,但没有任何变化:PermGen 内存仍在增加。
所以我可能遗漏了一些东西,点 (1-2) 和 (3) 之间的区别显示了我不明白的东西。我在哪里可以了解问题所在?由于我遵循的所有教程都显示了如何使用“搜索最近的 GC 根”功能来搜索泄漏的引用(在我的情况下没有),我不知道如何跟踪错误。
编辑 1:我在这里上传了一个堆转储示例。可以使用以下查询在 visualvm 中选择未卸载的 ClassLoader:select s from saierp.core.framework.system.SAITaskClassLoader s
可以看到有 4 个实例,并且应该收集前三个实例,因为没有 GC 根... 某处必须有引用,但我没有不知道如何搜索它。欢迎任何提示:)
编辑 2:经过一些更深入的测试后,我看到了一个非常奇怪的模式。泄漏似乎取决于 OpenJPA 正在加载的数据:如果没有加载新数据,则可以对类加载器进行 GC,否则不会。这是我在创建新的 SAITaskClassLoader 以“清除”旧的时使用的代码:
PCRegistry.deRegister(cl);
LogFactory.release(cl);
ResourceBundle.clearCache(cl);
Introspector.flushCaches();
= 模式 1(类加载器是 GCed): =
- 新的 SAITaskClassLoader
- 加载数据 D1, D2, ..., Dn
- 新的 SAITaskClassLoader
- 加载数据 D1, D2, ..., Dn
- ...
= 模式 2(类加载器未经过 GC):=
- 新的 SAITaskClassLoader
- 加载数据 D1、D2、D3
- 新的 SAITaskClassLoader
- 加载数据 D3、D4、D5
- 新的 SAITaskClassLoader
- 加载数据 D5、D6、D7
- ...
在所有情况下,已清除的 SAITaskClassLoader 都没有 GC 根。我们使用的是 OpenJPA 1.2.1。
谢谢和最好的问候