-
Bug
-
Resolution: Unresolved
-
Major
-
reported and reproduced in 1.532.2; reported in 1.21, reproduced in 1.24
A user reported an exhaustion of the permanent generation. jmap -histo:live showed that there were several hundred GroovyClassLoader instances in heap, and further analysis tracked down the problem to usage of the Job DSL plugin with Groovy mixins.
If you have a DSL
job { name 'generated' description("generated on ${new Date()}") }
then each time you run it, a new GroovyClassLoader is added to the heap. These do not seem to get collected easily, since they are held via a SoftReference in Groovy somewhere, but this /script forces soft references to be cleared:
for (i = 1; ; i *= 2) { System.gc(); new Object[i]; }
After running that, just one GroovyClassLoader remains, held from ASTTransformationVisitor.compUnit—an apparent memory leak in Groovy, but at least limited to holding the loader from one build at a time.
If I change the DSL to be
javaposse.jobdsl.dsl.Job.description2 = {String d -> description(d); } job { name 'generated' description2 "generated on ${new Date()}" }
(recommended monkey-patching) then I see the same behavior. But if I change the DSL to be
class JobMixin { public Object description2(String d) { return description(d.toUpperCase()); } } javaposse.jobdsl.dsl.Job.mixin(JobMixin); job { name 'generated' description2 "generated on ${new Date()}" }
then I get one GroovyClassLoader per run of the DSL, and they do not get collected even after forcing full GC. Besides the leak of one from ASTTransformationVisitor.compUnit, the rest are leaked in
this - value: groovy.lang.GroovyClassLoader #3 <- parent - class: groovy.util.GroovyScriptEngine$ScriptClassLoader, value: groovy.lang.GroovyClassLoader #3 <- delegate - class: groovy.lang.GroovyClassLoader$InnerLoader, value: groovy.util.GroovyScriptEngine$ScriptClassLoader #3 <- <classLoader> - class: JobMixin, value: groovy.lang.GroovyClassLoader$InnerLoader #3 <- cachedClass - class: org.codehaus.groovy.reflection.CachedClass, value: JobMixin class JobMixin <- mixinClass - class: org.codehaus.groovy.reflection.MixinInMetaClass, value: org.codehaus.groovy.reflection.CachedClass #150 <- key - class: java.util.LinkedHashMap$Entry, value: org.codehaus.groovy.reflection.MixinInMetaClass #3 <- [5] - class: java.util.HashMap$Entry[], value: java.util.LinkedHashMap$Entry #7716 <- table - class: java.util.LinkedHashMap, value: java.util.HashMap$Entry[] #7123 <- map - class: java.util.LinkedHashSet, value: java.util.LinkedHashMap #1581 <- mixinClasses - class: groovy.lang.ExpandoMetaClass, value: java.util.LinkedHashSet #45 <- strongMetaClass - class: org.codehaus.groovy.reflection.ClassInfo, value: groovy.lang.ExpandoMetaClass #1 <- $staticClassInfo - class: javaposse.jobdsl.dsl.Job, value: org.codehaus.groovy.reflection.ClassInfo #320 <- [353] - class: java.lang.Object[], value: javaposse.jobdsl.dsl.Job class Job <- elementData - class: java.util.Vector, value: java.lang.Object[] #8736 <- classes - class: hudson.PluginFirstClassLoader, value: java.util.Vector #192
(The analysis of root GC references was done using the NetBeans Profiler; VisualVM has the same tool.)
Suggested resolutions:
- Document this bug and close without fixing.
- Clear mixinClasses from every built-in DSL class after every DSL run. (Possibly a race condition, if you are running multiple DSLs at once.)
- Prevent DSLs from adding mixins, just throwing some informative error. (Again this would prevent a possible race condition in the current system: there seems to be nothing preventing unrelated DSLs from clashing over definitions.)
- Load DSL classes like Job in their own class loader for each DSL run, rather than loading them from the plugin class loader. That would ensure isolation between DSL runs, as well as preventing memory leaks. This seems like the best approach.