GroovyShell and memory leaks
Join the DZone community and get the full member experience.
Join For FreeTime to talk about creating new classes at runtime in Groovy. There seems to be some fear, uncertainty and doubt about memory leaks and evaluating code with Groovy in the form of calling an eval()
method.
The code that seems to cause consternation is this:
def shell = new GroovyShell() 1000.times { shell.evaluate "x = 100" }
The groovy.lang.GroovyShell
instance will call the parseClass()
method on an internal groovy.lang.GroovyClassLoader
instance, which will create a 1000 new classes. The classes will all extend the groovy.lang.Script
class.
With every new Class
created a little bit more memory will be used. As long as the groovy.lang.GroovyShell
instance and thus its internal groovy.lang.GroovyClassLoader
instance is not garbage collected this memory will remain occupied, even if you don't keep a reference to these classes. This is standard Java ClassLoader
behavior.
So, how to solve this problem? Well, ClassLoader
s in Java are somewhat hard to handle, but it's not so hard once you understand how they work. But lets also consider the root of the problem, namely the fact that Groovy creates a new Class
for each script that is evaluated.
Before answering why Groovy always creates new Class
objects when evaluating code let's first try to fix the code above.
One way to fix it is this:
def shell = new GroovyShell() 1000.times { shell.evaluate "x = 100" } shell = null
By setting the shell
variable to null
, will the GroovyClassLoader
instance be garbage collected? We can guarantee it will in this bit of code. But then again this code does not do anything useful :-)
Here's another way to fix it:
def shell = new GroovyShell() def script = shell.parse "x = 100" 1000.times { script.run() }
By evaluating - parsing - the code only once and calling the run()
method on the groovy.lang.Script
instance a 1000 times we only use 1/1000th of the memory :-) The parse()
method returns a groovy.lang.Script
instance.
But again, let's consider a more realistic use case. After all, the article that originally critized Groovy for causing memory leaks implies that evaluating code any number of times is a valid requirement in entreprise applications.
Let's say it's more of a corner case but still, the functionality is there and it can solve real-world problems. Evaluating Groovy code may be particularly useful and critical when evaluating code on demand. This could happen when an application reads code from file or a database to execute custom business logic.
Let's consider the case where developers create a DSL or Domain Specific Language like this:
assert customer instanceof Customer assert invoice instanceof Invoice letterHead { customer { name = customer.name address { line1 = "${customer.streetName}, ${customer.streetNumber}" line2 = "${customer.zipCode} ${customer.location}, ${customer.state}" } } invoiceSummary { number = invoice.id creationDate = invoice.createdOn dueDate = invoice.payableOn } }
To parse this DSL developers wrote this code (using the iText PDF library):
import com.lowagie.text.* import com.lowagie.text.pdf.* class LetterHeadFormatter { static byte[] createLetterHeadForInvoice(Customer cust, Invoice inv, String dsl) { Script dslScript = new GroovyShell().parse(dsl) dslScript.binding.variables.customer = cust dslScript.binding.variables.invoice = inv Document doc = new Document(PageSize.A4) def out = new ByteArrayOutputStream() PdfWriter writer = PdfWriter.getInstance(doc, out) doc.open() dslScript.metaClass = createEMC(writer, dslScript) dslScript.run() doc.close() return out.toByteArray() } static ExpandoMetaClass createEMC(PdfWriter writer, Script script) { ExpandoMetaClass emc = new ExpandoMetaClass(script.class, false) emc.letterHead = { Closure cl -> PdfContentByte content = writer.directContent cl.delegate = new LetterHeadDelegate(content) cl.resolveStrategy = Closure.DELEGATE_FIRST cl() } emc.initialize() return emc } }
(Check the attachements of this article to download this code. Read the README.txt
file if you want to run the load test yourself, and please report back the results. Also, check the PDF file for the output of the DSL.)
On line 6 the parse()
method is called. I wrote a load test that calls the createLetterHeadForInvoice()
method 1 million (!) times (with regular calls to System.gc()
). On my Windows XP machine, when I run the load test with Ant the java process memory usage fluctuates between 32 and 37Mb and remains stable over the course of several hours.
Are the GroovyShell
and internal GroovyClassLoader
instances garbage collected? Yes they are. Is there a memory leak? No.
So why does Groovy create Class
es when evaluating scripts? Every bit of code in Groovy is a java.lang.Class
. This means that it's loaded by a java.lang.ClassLoader
and remains in memory unless the ClassLoader
can be garbage collected. Why isn't a Class
object garbage collected as soon as it's no longer used? Why does the ClassLoader
itself have to be garbage collected before the classes it has loaded are removed from memory?
If classes would be automatically discarded and reloaded their static variables and static initialization would be executed on each reload. That would be quite surprising and unpredictable. That's why ClassLoader
s have to keep hold of their classes, to assure predictable behavior. There may be other technical reasons, but this is the most obvious one.
Once the ClassLoader
object itself gets garbage collected (because it's no longer referenced in any stack) the garbage collector will attempt to unload all its Class
objects.
Obviously, creating a lot of classes at runtime in the same ClassLoader
will increase the memory usage and will typically create a memory leak.
On the other hand, since every Groovy class is a real Java Class
(without exception) you don't have to make the distinction.
In conclusion: there is no memory leak in GroovyShell
or GroovyClassLoader
. Download the sample code and verify for yourself. Your code can create a memory leak by the way ClassLoader
s are used - either explicitly by your code or implicitly.
Opinions expressed by DZone contributors are their own.
Comments