package fr.infologic.jqbe.memoryleak; import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.MetaClassRegistry; import java.beans.Introspector; import java.io.InputStream; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.security.SecureClassLoader; import java.util.Map; import junit.framework.TestCase; import org.codehaus.groovy.runtime.Invoker; import org.codehaus.groovy.runtime.InvokerHelper; /** * The test for memory leak in MetaClassRegistry. The memory leak occurs due to the fact that values * stored in caches backed by WeakHashMap hold strong references to their keys. * * @author PSE */ public class GroovyMemoryLeakTest extends TestCase { /** * Tests the gc method. */ public void testGc() { Object o = new Object(); WeakReference ref = new WeakReference(o); o = null; gc(ref); assertNull(ref.get()); } /** * Test method called using Groovy */ public int number() { return 1; } /** * Test the memory leak in the MetaClassRegistry */ public void testMemoryLeak() throws Exception { // create a throw-away class loader. MemoryLeakClassLoader classLoader = new MemoryLeakClassLoader(getClass().getClassLoader()); // load an instantiate a class using this class loader (for convinience this class is // GroovyMemoryLeakTest) Object object = classLoader.loadSelf().newInstance(); // assert that the class is indeed loaded using our class loader assertEquals(classLoader, object.getClass().getClassLoader()); // setup a variable that holds our test object Binding binding = new Binding(); binding.setVariable("test", object); // evaluate a groovy script that calls the number() method on our object GroovyShell shell = new GroovyShell(binding); Number number = (Number) shell.evaluate("test.number()"); // assert that it has worked assertEquals(1, number.intValue()); // keep a weak reference to our class loader WeakReference ref = new WeakReference(classLoader); // flush Introspector caches (see Introspector javadoc for more details) Introspector.flushFromCaches(object.getClass()); // release all strong references classLoader = null; object = null; binding = null; shell = null; // Uncomment this to test that the test indeed passes when the groovy caches are cleaned //cleanGroovyCaches(); // force the GC gc(ref); // assert that the throw-away class loader was collected assertNull(ref.get()); } private void cleanGroovyCaches() throws Exception { Invoker invoker = InvokerHelper.getInstance(); MetaClassRegistry registry = invoker.getMetaRegistry(); ((Map) getPrivateFieldValue(registry, "metaClasses")).clear(); ((Map) getPrivateFieldValue(registry, "loaderMap")).clear(); } /** * Performs the garbage collection. Inspired from some Apache project. Basically forces the * garbage collection by filling-in the available memory with garbage. The method stops when the * passed in weak reference is collected or an OutOfMemoryError occurs which means that the * reference is strongly hold somewhere else. */ private void gc(WeakReference ref) { System.gc(); int size = 1024; byte[] buffer = null; while (ref.get() != null) { System.gc(); try { buffer = new byte[size]; size = size * 2; } catch (OutOfMemoryError e) { break; } } buffer = null; System.gc(); } /** * A custom throw-away class loader */ public class MemoryLeakClassLoader extends SecureClassLoader { public MemoryLeakClassLoader(ClassLoader parentClassLoader) { super(parentClassLoader); } /** * Loads the GroovyMemoryLeakTest class using this classloader. This way we do not need * other classes to perform our test. */ public Class loadSelf() throws Exception { String fullClassName = GroovyMemoryLeakTest.class.getName(); int dotIndex = fullClassName.lastIndexOf('.'); String className = dotIndex == -1 ? fullClassName : fullClassName .substring(dotIndex + 1); InputStream in = GroovyMemoryLeakTest.class.getResourceAsStream(className + ".class"); byte[] bytes = new byte[1024 * 8]; int length = in.read(bytes); return defineClass(fullClassName, bytes, 0, length, GroovyMemoryLeakTest.class .getProtectionDomain().getCodeSource()); } } /** * Uses reflection to return the value of the protected field of the given bean. */ public static Object getPrivateFieldValue(Object bean, String fieldName) throws Exception { Field field = null; Field[] fields = bean.getClass().getDeclaredFields(); for (int i = 0; i < fields.length; i++) { if (fields[i].getName().equals(fieldName)) { field = fields[i]; break; } } if (field == null) throw new RuntimeException("No such field: " + fieldName + " in class " + bean.getClass().getName()); if (!field.isAccessible()) field.setAccessible(true); return field.get(bean); } }