类加载器即使没有GC根也不会被垃圾回收

8
我们有一个在Glassfish V2.1.1下运行的复杂应用程序。为了能够动态加载代码,我们实现了一个CustomClassloader,它能够重新定义类。
行为非常简单:当动态加载的类发生更改时,当前CustomClassloader实例被“丢弃”,并创建一个新的实例来重新定义所需的类。
这很有效,但是在同一类被重新加载了几次之后(因此每次都会创建一个新的CustomClassloader实例),我们会出现PermGen空间错误,因为其他CustomClassloader实例没有被垃圾回收。(应该只有一个此类的实例)
我尝试了不同的方法来追踪泄漏的位置:
  1. visualvm => 我创建了一个堆转储并提取了CustomClassloader的所有实例。我发现它们中没有一个被finalize。当我检查最近的GC根时,visualvm告诉我没有(除了最后一个实例,因为它是“真正”使用的实例)。
  2. jmap/jhat => 它给我几乎相同的结果:我看到CustomClassloader的所有实例,然后当我单击其中一个实例的引用链接时,我得到一个空白页面,表示没有引用...
  3. 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加载的数据:如果没有加载新数据,则类加载器可以被GCed,否则就不行。这是我创建新的SAITaskClassLoader时使用的代码,以“清除”旧的代码:

PCRegistry.deRegister(cl);
LogFactory.release(cl);
ResourceBundle.clearCache(cl);
Introspector.flushCaches();

= 模式 1(类加载器被垃圾回收): =

  1. 创建新的 SAITaskClassLoader
  2. 加载数据 D1、D2、...、Dn
  3. 创建新的 SAITaskClassLoader
  4. 加载数据 D1、D2、...、Dn
  5. ...

cl-gc

= 模式2(Classloader不会被GC):=

  1. 新建SAITaskClassLoader
  2. 加载数据D1,D2,D3
  3. 新建SAITaskClassLoader
  4. 加载数据D3,D4,D5
  5. 新建SAITaskClassLoader
  6. 加载数据D5,D6,D7
  7. ...

cl-nogc

在所有情况下,已经被清除的SAITaskClassLoader没有GC根。我们正在使用OpenJPA 1.2.1。
谢谢&最好的祝福

你尝试过单例模式吗? - OmniOwl
是的,实际上CustomClassloader是一个单例。 - ctabin
很奇怪。如果那是这样的话,您就永远不应该能够拥有多个从未完成的此类型对象。第一个对象会阻止您...嗯。您尝试将丢弃的对象设置为 null 了吗? - OmniOwl
是的,这并不会改变任何事情。此外,在我上面提到的模式中,我加载了相同数量的数据。因此,PermGen 将增加,直到没有新数据被加载... - ctabin
我创建了一个小项目,重现了这个错误在这里 - ctabin
3个回答

7
没有CustomClassLoader的源代码片段或实际堆转储,将很难跟踪问题。你的CustomClassLoader不能是单例模式。如果是,你的设计无法工作(或者我错过了什么)。
你需要获取类型为CustomClassLoaderClassLoader实例列表,并跟踪这些对象的引用。
这些帖子可能会帮助你进一步分析并深入研究如何追踪ClassLoader泄漏的细节:

感谢你的链接。当我说CustomClassloader是单例时,从外部类来看是正确的,获取它的唯一方法是使用静态getInstance()方法。当有类变更时,它将能够检测到,并在内部创建一个新实例,该实例将存储在静态内部变量中。后者是整个应用程序中唯一显式引用。 - ctabin
也许我找到了点头绪。我正在使用OpenJPA 1.2.1,发现了这个bug:https://issues.apache.org/jira/browse/GERONIMO-3326 - ctabin
我已经上传了我的堆的ZIP文件,这样你就可以看一下了。 - ctabin
这是一次意外的尝试:似乎有一些引用,例如从BusinessTask到SAITaskClassLoader到ResourceBundle。偶尔尝试使用ResourceBundler.clearCache(saiTaskClassLoader)。另一个问题可能是java.util.Logger。Glassfish正在使用自己的ServerLogManager,可能出了什么问题? (Heap dump object 0x784de8310和0x7070236c8)。也许这正是这个漏洞:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6717784 - mhaller
感谢您的回复。我已经添加了ResourceBundle.clearCache,但这没有改变任何事情。关于glassfish bug,在这种情况下,visualvm中会显示GC roots... 我建立了一个压力测试,强制创建一个新的SAITaskLoader,并监视PermGen:似乎泄漏与数据有关:如果不加载新数据,则在创建新数据时可以收集类加载器。我看到SAITaskClassLoader的第一个实例从未被收集,但是其他大部分实例都被收集了。 但这并不能解释为什么堆分析器中没有显示根GC :( - ctabin

3
垃圾收集器对类加载器的处理非常棘手。使用 JProfiler,我看到以下链式引用到当前活动的自定义类加载器:

enter image description here

这说明你在自定义类加载器中有一个静态字段"singleInstance",它引用了类加载器本身。您应该尝试在重新部署时清除该字段,以便更容易地让VM收集类加载器。
关于您在Eclipse MAT中得到的结果的注释:它会删除所有不是强可达的对象。JProfiler默认也是这样做的。因此,前三个类加载器应该被垃圾回收,但它们没有被回收,这是由于JVM对类加载器GC的特殊规则,并未被堆中标准引用所捕获。
免责声明:我的公司开发了JProfiler

1
感谢您的回复。然而,这是正确的 :) SAITaskClassLoader有一个实例被保存在静态变量中。当类被更新时,该实例被丢弃,然后在此[静态]变量中存储一个新实例。问题在于,已删除的实例没有被垃圾收集器回收,我测试过的所有堆分析器都显示它们没有任何GC根...在转储中,4个实例中的3个应该已经被收集,但从未被收集,导致在创建一些新的SAITaskClassLoader实例后出现PermGen OutOfMemoryError。 - ctabin
根据您的实现方式,这可能仍然存在问题。这不一定是关于GC根路径的问题,类加载器GC除了强可达性之外还有不同的规则,而且非常脆弱。也许您可以插入另一层,该层本身不会重新加载,并在那里保存类加载器的单例实例,这样您就可以更明确地控制它。 - Ingo Kegel
我已经识别出了一个模式并添加了一些图表,也许有人可以从中得到一些想法,知道该往哪里寻找... - ctabin
你有关于这个主题的任何其他信息吗?我遇到了类似的问题。一个类加载器和只有弱引用指向它,但它没有被垃圾回收。 - Falco

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接