我有一个Java类,它使用自定义类加载器动态重新加载groovy类,我看到一些奇怪的行为,某些类没有被收集,但随着时间的推移,它不会泄漏内存(例如,perm gen不会无限期地继续增长)。
在我的java代码中,我正在加载这样的类(为了简单起见,删除了样板文件):
Class clazz = groovyClassLoader.loadClass(className, true, false, true);
instance = clazz.newInstance();
然后我通过清除类加载器缓存,metaregistry等动态重新加载groovy类:
for (Class c : groovyClassLoader.getLoadedClasses()){
GroovySystem.getMetaClassRegistry().removeMetaClass(c);
}
groovyClassLoader.clearCache();
现在,如果我只是循环遍历这段代码,不断加载然后重新加载我的groovy类,我会看到奇怪的行为(我的测试代码实际上只是循环重载过程 - 它没有对任何对象做任何事情创建等,所以上面的代码中的实例只是本地的,所以应该对GC有好处。
如果我运行它,将maxpermsize设置为128m然后我得到泄漏行为和OOM permgen错误:
然而,如果我再次运行它并将maxpermsize增加到256m,那么一切都很好并且它可以永远运行(这个图像是1小时,但我已经运行了一夜,做了数千次重新加载):
有没有人遇到任何类似的行为?还是有什么想法?在第一个例子中,内存使用量逐步上升而不是稳定增长,这似乎也很奇怪。
答案 0 :(得分:3)
您看到的锯齿形图案是典型的内存分配和释放。您添加了清除缓存的代码,但这并不意味着会收集该类。在为更长的生活对象尝试正常的垃圾收集之前,JVM倾向于大量增加内存。删除类的工作量更少,通常只在完整的gc运行期间。这可能会导致令人讨厌的情况,即permgen已经满了并且OOME被抛出,即使已经收集了类。确切的行为似乎因版本而异。
反正。仅因为该类不再被引用,并不意味着它立即被收集。相反,permgen可能会增长到最大值然后卸载类。
除了你的loadClass调用可能导致创建一个新类以及以某种方式引用该类的元类注册表之外,还有更多具有可能引用的类。例如,Groovy中的调用点缓存也涉及到类的SoftReferences。如果反射(Groovy可能必须这样做)经常调用某些东西,可能会生成辅助类来加速反射。后面这个是由JDK完成的,而不是Groovy。
我必须做出一个修正:元类不是真正的类,也不能采用permgen。但是他们引用了带有permgen的类。因此,如果元类很难被引用,则该类保持不变。 IBM JDK中有一个有趣的“特性”,如果它被硬引用,它确实将类视为可卸载的,即使执行该引用的对象本身也是SoftReference的一部分。
为了解释上面的行为更完整,我需要JVM的输出来进行类加载和卸载。我认为应用程序理论上可以运行128MB。如果您在16:17:30之前的低点和之前的那个位置查看128MB图表,您可能会注意到之前的那个并不像另一个那么低。这意味着直到该时间代码之前的点卸载了比之前的点更多的类。 JVM可以自由决定何时删除一个类,并不总是删除理论上可以卸载的所有类。你必须在可以和性能下卸载的类之间进行权衡。