Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java / Groovy - Memory leak in GroovyClassLoader

I'm loading a large number of Groovy (2.4.6) scripts and running them using GroovyScriptEngineImpl in my Java 8 application and I'm running into a problem after a while.

There is a few things that you need to know :

  • I have to recreate a new GroovyScriptEngineImpl every time I run a script
  • I have to recreate a new GroovyClassLoader every time I run a script

I need to do it like that in order to isolate each script in a separate "environment": I'm loading some external JARs in the classloader for some scripts and I don't want other scripts to be able to use the classes in those JARs when they're executed.

My problem comes from the fact that for every script that I run, the GroovyClassLoader will create a new ScriptXXXX class and load it, but never unload it.

This results in the number of classes loaded increasing indefinitly and the memory ending up being totally filled.

I have tried a huge amount of various solutions, but none seem to work :

  • Adding -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC in the JVM arguments
  • Adding -Dgroovy.use.classvalue=true in the JVM arguments
  • Removing the "meta classes" created for each ScriptXXXX class as shown here : Groovy Classes not being collected but not signs of memory leak
  • Clearing the cache and closing the GroovyClassLoader
  • Using introspection to manually clear some fields caching the classes in the GroovyScriptEngineImpl
  • etc...

Here's the "Shortest path to GC" for one of the ScriptXXXX class in Eclipse Memory Analyzer :

Eclipse Memory Analyzer dump

I'm clearly running out of solutions here, and none seem to really work as the class loader always keeps a reference to the classes which are never getting GCed.

If you want to reproduce the issue, here's a code sample :

GroovyScriptEngineImpl se;

while (true)
{
    se = new GroovyScriptEngineImpl(new GroovyClassLoader());
    CompiledScript script = se.compile("println(\"hello\")");
    script.eval(se.createBindings());
}

Thanks

UPDATE : After reading pczeus's reply, I tried limiting the metaspace, and some classes seem to be unloading indeed, and I think that it's the ScriptXXX classes.

That said, after a few minutes I'm getting Out of Metaspace errors during the script execution.

Here's the profile I'm getting with the VisualVM :

Profile in the Java VisualVM

And the "Path to GC" in Eclipse Memory Analyzer for the ScriptXXX classes are indeed empty (their's no more instance of the classes), even tho the class is still listed in the histogram.

like image 749
AntoineB Avatar asked May 18 '16 13:05

AntoineB


People also ask

How do you fix a memory leak in Java?

Use reference objects to avoid memory leaks Using the java. lang. ref package, you can work with the garbage collector in your program. This allows you to avoid directly referencing objects and use special reference objects that the garbage collector easily clears.

Does unclosed streams cause memory leak in Java?

Through Unclosed Resources Whenever you create a new connection or open a stream, the JVM allocates memory for these resources. In such cases, if you forget to close these resources can block the memory, reduce performance, and may even result in OutOfMemoryError.

How do you detect memory leaks?

The primary tools for detecting memory leaks are the C/C++ debugger and the C Run-time Library (CRT) debug heap functions. The #define statement maps a base version of the CRT heap functions to the corresponding debug version. If you leave out the #define statement, the memory leak dump will be less detailed.


1 Answers

This definitely appears to be somewhat of an edge case that requires special tuning and possibly choosing the garbage collection scheme used.

Since you are using Java8, and you know you are loading thousands of temporary classes, you should try to tune and limit the amount of MetaSpace available and tune the frequency of cleanup. Straight from https://blogs.oracle.com/poonam/entry/about_g1_garbage_collector_permanent :

JDK8: Metaspace

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace. There are some new flags added for Metaspace in JDK 8:

-XX:MetaspaceSize= where is the initial amount of space(the initial high-water-mark) allocated for class metadata (in bytes) that may induce a garbage collection to unload classes. The amount is approximate. After the high-water-mark is first reached, the next high-water-mark is managed by the garbage collector

-XX:MaxMetaspaceSize= where is the maximum amount of space to be allocated for class metadata (in bytes). This flag can be used to limit the amount of space allocated for class metadata. This value is approximate. By default there is no limit set. -XX:MinMetaspaceFreeRatio= where is the minimum percentage of class metadata capacity free after a GC to avoid an increase in the amount of space (high-water-mark) allocated for class metadata that will induce a garbage collection.

-XX:MaxMetaspaceFreeRatio= where is the maximum percentage of class metadata capacity free after a GC to avoid a reduction in the amount of space (high-water-mark) allocated for class metadata that will induce a garbage collection.

By default class metadata allocation is only limited by the amount of available native memory. We can use the new option MaxMetaspaceSize to limit the amount of native memory used for the class metadata. It is analogous to MaxPermSize. A garbage collection is induced to collect the dead classloaders and classes when the class metadata usage reaches MetaspaceSize (12Mbytes on the 32bit client VM and 16Mbytes on the 32bit server VM with larger sizes on the 64bit VMs). Set MetaspaceSize to a higher value to delay the induced garbage collections. After an induced garbage collection, the class metadata usage needed to induce the next garbage collection may be increased.

like image 195
pczeus Avatar answered Sep 20 '22 06:09

pczeus