了解Groovy / Grails类加载器泄漏

昨天我将我的第一个Grails(2.3.6)应用程序部署到开发服务器并开始监控它。 我刚刚得到一个自动监视器,说明CPU被固定在这台机器上,所以我连接到它。 我跑了top ,发现是我的Java应用程序的PID固定服务器。 我也注意到内存是40%。 几秒钟后,CPU停止固定,降至正常水平,内存恢复到~20%范围内。 经典主要GC。

在收集的时候,我做了堆转储。 在GC之后,我在JVisualVM中打开了转储,看到大部分内存都是为org.codehaus.groovy.runtime.metaclass.MetaMethodIndex.Entry类分配的。 总共有近250,000个这样的实例,占用了大约25 MB的内存。

我用Google搜索了这个课程并看了一下它是非常有帮助的Javadocs 。 所以我仍然不知道这门课的作用。

但谷歌搜索它还带来了大约十几篇涉及这个类的相关文章(其中一些是SO问题)和Grails / Groovy应用程序的PermGen / classloader泄漏。 虽然看起来我的应用程序确实用GC清理了这些250K实例,但仍然令人不安的是它有如此多的实例,并且GC将CPU固定超过5分钟。

我的问题:

  • 什么是这个类,Groovy用它做什么?
  • 有人可以向我解释这个答案吗? 为什么-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled帮助解决这个特殊问题?
  • 为什么这个课程对PermGen特别麻烦?

Groovy是一种动态语言,每个方法调用都是动态调度的。 为了优化Groovy,为MetaClass中的每个java.lang.Class创建一个MetaClassRegistry 。 这些MetaClass实例是按需创建的,并使用弱引用进行存储。

你看到很多org.codehaus.groovy.runtime.metaclass.MetaMethodIndex.Entry的原因是因为Groovy在内存中存储了类和方法的映射,以便运行时可以快速调度它们。 根据应用程序的大小,这可能是因为您已经发现了数千个类,因为每个类可能有几十个有时数百个方法。

但是,Groovy和Grails中没有“内存泄漏”,你看到的是正常行为。 您的应用程序内存不足,可能是因为它没有分配足够的内存,这反过来会导致MetaClass实例被垃圾回收。 现在说例如你有一个循环:

 for(str in strings) { println str.toUpperCase() } 

在这种情况下,我们在String类上调用一个方法。 如果内存不足,将会发生的情况是,对于循环的每次迭代, MetaClass将被垃圾收集,然后再次重新创建以用于下一次迭代。 这可能会大大减慢应用程序的速度,并导致CPU被固定,如您所见。 此状态通常称为“元类流失”,是应用程序在堆内存上运行不足的标志。

如果Groovy 不是垃圾收集这些MetaClass实例,那么是的,这意味着Groovy中存在内存泄漏,但事实上它是垃圾收集这些类,这表明一切都很好,除了你没有足够的分配堆内存首先。 这并不是说应用程序的另一部分可能存在内存泄漏,这会占用所有可用内存,并且不足以让Groovy正常运行。

至于你提到的另一个答案,添加类卸载和PermGen调整实际上不会做任何事情来解决你的内存问题, 除非你在运行时动态解析类。 JVM使用PermGen空间来存储动态创建的类。 Groovy允许您使用GroovyClassLoader.parseClassGroovyShell.evaluate在运行时编译类。 如果你不断解析类,那么添加类卸载标志可以提供帮助。 另见这篇文章:

使用死Groovy代码定位填充PermGen的代码

但是,典型的Grails应用程序不会在运行时动态编译类,因此调整PermGen和类卸载设置实际上不会实现任何效果。

您应该使用-Xmx标志validation是否已分配了足够的堆内存,如果没有分配更多内存。