Stuart McCulloch contributed a custom classloader that fixes the memory leak and supports OSGi-style classloaders (plus any other form of non-delegation).

Now when Guice loads its own classes (for AOP, etc.), those classes will get garbage collected properly when they go out of scope.


git-svn-id: https://google-guice.googlecode.com/svn/trunk@538 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/build.xml b/build.xml
index 28620b7..e9dcaff 100644
--- a/build.xml
+++ b/build.xml
@@ -21,9 +21,9 @@
       <zipfileset src="lib/build/asm-3.1.jar"/>
       <zipfileset src="lib/build/google-collect-snapshot-20080530.jar"/>
       <rule pattern="com.google.common.**" result="com.google.inject.internal.@1"/>
-      <rule pattern="net.sf.cglib.**" result="com.google.inject.cglib.@1"/>
-      <rule pattern="org.objectweb.asm.**" result="com.google.inject.asm.@1"/>
-      <keep pattern="com.google.**"/>
+      <rule pattern="net.sf.cglib.**" result="com.google.inject.internal.cglib.@1"/>
+      <rule pattern="org.objectweb.asm.**" result="com.google.inject.internal.asm.@1"/>
+      <keep pattern="com.google.inject.**"/>
     </jarjar>
   </target>
 
@@ -89,8 +89,12 @@
         <pathelement location="lib/build/junit.jar"/>
         <pathelement location="lib/build/servlet-api-2.5.jar"/>
         <pathelement location="lib/build/easymock.jar"/>
+        <pathelement location="lib/build/google-collect-snapshot-20080530.jar"/>
       </classpath>
       <arg value="com.google.inject.AllTests"/>    
+      <syspropertyset>
+        <propertyref name="guice.custom.loader"/>
+      </syspropertyset>
     </java>
   </target>
   
diff --git a/common.xml b/common.xml
index 402e54d..cc7287c 100644
--- a/common.xml
+++ b/common.xml
@@ -41,7 +41,7 @@
     <property name="exclude.imports" value=""/>
     <property name="guice.imports" value="com.google.inject.*;version=${api.version}"/>
     <property name="Import-Package" value="${exclude.imports},${guice.imports},*;resolution:=optional"/>
-    <property name="Export-Package" value="${module}.*;version=${api.version}"/>
+    <property name="Export-Package" value="!${module}.internal.*,${module}.*;version=${api.version}"/>
     <property name="-nouses" value="true"/>
     <property name="-removeheaders" value="Bnd-LastModified,Ignore-Package,Include-Resource,Private-Package,Tool"/>
 
diff --git a/guice.iml b/guice.iml
index f4d0954..cb08b03 100644
--- a/guice.iml
+++ b/guice.iml
@@ -72,7 +72,9 @@
           <root url="file://$MODULE_DIR$/lib/build" />
         </CLASSES>
         <JAVADOC />
-        <SOURCES />
+        <SOURCES>
+          <root url="jar://$MODULE_DIR$/lib/build/cglib-src-2.2.jar!/src/proxy" />
+        </SOURCES>
         <jarDirectory url="file://$MODULE_DIR$/lib/build" recursive="false" />
       </library>
     </orderEntry>
diff --git a/src/com/google/inject/ConstructionContext.java b/src/com/google/inject/ConstructionContext.java
index 55c2b45..bbcb335 100644
--- a/src/com/google/inject/ConstructionContext.java
+++ b/src/com/google/inject/ConstructionContext.java
@@ -16,6 +16,7 @@
 
 package com.google.inject;
 
+import com.google.inject.internal.BytecodeGen;
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ErrorsException;
 import java.lang.reflect.InvocationHandler;
@@ -79,9 +80,9 @@
         = new DelegatingInvocationHandler<T>();
     invocationHandlers.add(invocationHandler);
 
-    Object object = Proxy.newProxyInstance(expectedType.getClassLoader(),
-        new Class[] { expectedType }, invocationHandler);
-    return expectedType.cast(object);
+    ClassLoader classLoader = BytecodeGen.getClassLoader(expectedType);
+    return expectedType.cast(Proxy.newProxyInstance(classLoader,
+        new Class[] { expectedType }, invocationHandler));
   }
 
   void setProxyDelegates(T delegate) {
diff --git a/src/com/google/inject/DefaultConstructionProxyFactory.java b/src/com/google/inject/DefaultConstructionProxyFactory.java
index 33bfdc8..ace1f93 100644
--- a/src/com/google/inject/DefaultConstructionProxyFactory.java
+++ b/src/com/google/inject/DefaultConstructionProxyFactory.java
@@ -16,9 +16,9 @@
 
 package com.google.inject;
 
+import static com.google.inject.internal.BytecodeGen.newFastClass;
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ErrorsException;
-import com.google.inject.internal.GuiceFastClass;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Modifier;
@@ -67,7 +67,7 @@
 
     return new ConstructionProxy<T>() {
       Class<T> classToConstruct = constructor.getDeclaringClass();
-      FastClass fastClass = GuiceFastClass.create(classToConstruct);
+      FastClass fastClass = newFastClass(classToConstruct);
       final FastConstructor fastConstructor = fastClass.getConstructor(constructor);
 
       @SuppressWarnings("unchecked")
diff --git a/src/com/google/inject/InjectorImpl.java b/src/com/google/inject/InjectorImpl.java
index f2180f0..d337d85 100644
--- a/src/com/google/inject/InjectorImpl.java
+++ b/src/com/google/inject/InjectorImpl.java
@@ -23,10 +23,10 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Sets;
+import static com.google.inject.internal.BytecodeGen.newFastClass;
 import com.google.inject.internal.Classes;
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ErrorsException;
-import com.google.inject.internal.GuiceFastClass;
 import com.google.inject.internal.Keys;
 import com.google.inject.internal.MatcherAndConverter;
 import com.google.inject.internal.Nullability;
@@ -883,9 +883,8 @@
             return method.invoke(target, parameters);
           }
         };
-      }
-      else {
-        FastClass fastClass = GuiceFastClass.create(method.getDeclaringClass());
+      } else {
+        FastClass fastClass = newFastClass(method.getDeclaringClass());
         final FastMethod fastMethod = fastClass.getMethod(method);
 
         methodInvoker = new MethodInvoker() {
@@ -1099,10 +1098,9 @@
       reference[0] = new InternalContext(this);
       try {
         return callable.call(reference[0]);
-      }
-      finally {
+      } finally {
         // Only remove the context if this call created it.
-        reference[0] = null;
+        localContext.remove();
       }
     }
     else {
diff --git a/src/com/google/inject/ProxyFactory.java b/src/com/google/inject/ProxyFactory.java
index 48fc94f..13bb646 100644
--- a/src/com/google/inject/ProxyFactory.java
+++ b/src/com/google/inject/ProxyFactory.java
@@ -18,10 +18,10 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import static com.google.inject.internal.BytecodeGen.newEnhancer;
+import static com.google.inject.internal.BytecodeGen.newFastClass;
 import com.google.inject.internal.Errors;
 import com.google.inject.internal.ErrorsException;
-import com.google.inject.internal.GuiceFastClass;
-import com.google.inject.internal.GuiceNamingPolicy;
 import com.google.inject.internal.ReferenceCache;
 import com.google.inject.internal.StackTraceElements;
 import java.lang.reflect.Constructor;
@@ -147,17 +147,13 @@
     }
 
     // Create the proxied class.
-    Enhancer enhancer = new Enhancer();
-    enhancer.setSuperclass(declaringClass);
-    enhancer.setUseCache(false); // We do enough caching.
+    Enhancer enhancer = newEnhancer(declaringClass);
     enhancer.setCallbackFilter(new CallbackFilter() {
       public int accept(Method method) {
         return indices.get(method);
       }
     });
     enhancer.setCallbackTypes(callbackTypes);
-    enhancer.setUseFactory(false);
-    enhancer.setNamingPolicy(new GuiceNamingPolicy());
 
     Class<?> proxied = enhancer.createClass();
 
@@ -172,7 +168,7 @@
    */
   private <T> ConstructionProxy<T> createConstructionProxy(Errors errors, final Class<?> clazz,
       final Constructor standardConstructor) throws ErrorsException {
-    FastClass fastClass = GuiceFastClass.create(clazz);
+    FastClass fastClass = newFastClass(clazz);
     final FastConstructor fastConstructor
         = fastClass.getConstructor(standardConstructor.getParameterTypes());
     final List<Parameter<?>> parameters = Parameter.forConstructor(standardConstructor, errors);
diff --git a/src/com/google/inject/internal/BytecodeGen.java b/src/com/google/inject/internal/BytecodeGen.java
new file mode 100644
index 0000000..47a5ef2
--- /dev/null
+++ b/src/com/google/inject/internal/BytecodeGen.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.inject.internal;
+
+import static com.google.inject.internal.ReferenceType.WEAK;
+import static java.lang.reflect.Modifier.PROTECTED;
+import static java.lang.reflect.Modifier.PUBLIC;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import net.sf.cglib.core.DefaultNamingPolicy;
+import net.sf.cglib.core.NamingPolicy;
+import net.sf.cglib.proxy.Enhancer;
+import net.sf.cglib.reflect.FastClass;
+
+/**
+ * Utility methods for runtime code generation and class loading. We use this stuff for faster
+ * reflection ({@link FastClass}), method interceptors ({@link Enhancer}) and to proxy circular
+ * dependencies.
+ *
+ * <p>When loading classes, we need to be careful of:
+ * <ul>
+ *   <li><strong>Memory leaks.</strong> Generated classes need to be garbage collected in long-lived
+ *       applications. Once an injector and any instances it created can be garbage collected, the
+ *       corresponding generated classes should be collectable.
+ *   <li><strong>Visibility.</strong> Containers like <code>OSGi</code> use class loader boundaries
+ *       to enforce modularity at runtime.
+ * </ul>
+ *
+ * <p>For each generated class, there's multiple class loaders involved:
+ * <ul>
+ *    <li><strong>The related class's class loader.</strong> Every generated class services exactly
+ *        one user-supplied class. This class loader must be used to access members with private and
+ *        package visibility.
+ *    <li><strong>Guice's class loader.</strong>
+ *    <li><strong>Our bridge class loader.</strong> This is a child of the user's class loader. It
+ *        selectively delegates to either the user's class loader (for user classes) or the Guice
+ *        class loader (for internal classes that are used by the generated classes). This class
+ *        loader that owns the classes generated by Guice.
+ * </ul>
+ *
+ * @author mcculls@gmail.com (Stuart McCulloch)
+ * @author jessewilson@google.com (Jesse Wilson)
+ */
+public final class BytecodeGen {
+
+  static final ClassLoader GUICE_CLASS_LOADER = BytecodeGen.class.getClassLoader();
+
+  /** ie. "com.google.inject.internal" */
+  private static final String GUICE_INTERNAL_PACKAGE
+      = BytecodeGen.class.getName().replaceFirst("\\.internal\\..*$", ".internal");
+
+  /** either "net.sf.cglib", or "com.google.inject.internal.cglib" */
+  private static final String CGLIB_PACKAGE
+      = Enhancer.class.getName().replaceFirst("\\.cglib\\..*$", ".cglib");
+
+  static final NamingPolicy NAMING_POLICY = new DefaultNamingPolicy() {
+    @Override protected String getTag() {
+      return "ByGuice";
+    }
+  };
+
+  /** Use "-Dguice.custom.loader=false" to disable custom classloading. */
+  static final boolean HOOK_ENABLED
+      = "true".equals(System.getProperty("guice.custom.loader", "true"));
+
+  /**
+   * Weak cache of bridge class loaders that make the Guice implementation
+   * classes visible to various code-generated proxies of client classes.
+   */
+  private static final ReferenceCache<ClassLoader, ClassLoader> CLASS_LOADER_CACHE
+      = new ReferenceCache<ClassLoader, ClassLoader>(WEAK, WEAK) {
+        @Override protected ClassLoader create(final ClassLoader typeClassLoader) {
+          return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
+            public ClassLoader run() {
+              return new BridgeClassLoader(typeClassLoader);
+            }
+          });
+        }
+      };
+
+  /**
+   * Returns true if {@code type} can be loaded from our custom class loader.
+   * Private/implementation classes are not visible to classes loaded by other
+   * class loaders, even when they are in the same package. Therefore we cannot
+   * intercept classloading requests for such types.
+   */
+  private static boolean isHookable(Class<?> type) {
+    return (type.getModifiers() & (PROTECTED | PUBLIC)) != 0;
+  }
+
+  /**
+   * For class loaders, {@code null}, is always an alias to the
+   * {@link ClassLoader#getSystemClassLoader() system class loader}.
+   */
+  private static ClassLoader canonicalize(ClassLoader classLoader) {
+    return classLoader != null
+        ? classLoader
+        : ClassLoader.getSystemClassLoader();
+  }
+
+  /**
+   * Returns the class loader to host generated classes for {@code type}.
+   */
+  public static ClassLoader getClassLoader(Class<?> type) {
+    return getClassLoader(type, type.getClassLoader());
+  }
+
+  private static ClassLoader getClassLoader(Class<?> type, ClassLoader delegate) {
+    delegate = canonicalize(delegate);
+
+    if (HOOK_ENABLED && isHookable(type)) {
+      return CLASS_LOADER_CACHE.get(delegate);
+    }
+
+    return delegate;
+  }
+
+  public static FastClass newFastClass(Class<?> type) {
+    FastClass.Generator generator = new FastClass.Generator();
+    generator.setType(type);
+    generator.setClassLoader(getClassLoader(type));
+    generator.setNamingPolicy(NAMING_POLICY);
+    return generator.create();
+  }
+
+  public static Enhancer newEnhancer(Class<?> type) {
+    Enhancer enhancer = new Enhancer();
+    enhancer.setSuperclass(type);
+    enhancer.setUseCache(false); // We do enough caching.
+    enhancer.setUseFactory(false);
+    enhancer.setNamingPolicy(NAMING_POLICY);
+    enhancer.setClassLoader(getClassLoader(type));
+    return enhancer;
+  }
+
+  /**
+   * Loader for Guice-generated classes. For referenced classes, this delegates to either either the
+   * user's classloader (which is the parent of this classloader) or Guice's class loader.
+   */
+  private static class BridgeClassLoader extends ClassLoader {
+
+    public BridgeClassLoader(ClassLoader usersClassLoader) {
+      super(usersClassLoader);
+    }
+
+    @Override protected Class<?> loadClass(String name, boolean resolve)
+        throws ClassNotFoundException {
+
+      // delegate internal requests to Guice class space
+      if (name.startsWith(GUICE_INTERNAL_PACKAGE) || name.startsWith(CGLIB_PACKAGE)) {
+        try {
+          Class<?> clazz = GUICE_CLASS_LOADER.loadClass(name);
+          if (resolve) {
+            resolveClass(clazz);
+          }
+          return clazz;
+        } catch (Exception e) {
+          // fall back to classic delegation
+        }
+      }
+
+      return super.loadClass(name, resolve);
+    }
+  }
+}
diff --git a/src/com/google/inject/internal/GuiceFastClass.java b/src/com/google/inject/internal/GuiceFastClass.java
deleted file mode 100644
index 4495ae8..0000000
--- a/src/com/google/inject/internal/GuiceFastClass.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (C) 2006 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.inject.internal;
-
-import net.sf.cglib.reflect.FastClass;
-
-/**
- * Gives Guice classes custom names.
- *
- * @author crazybob@google.com (Bob Lee)
- */
-public class GuiceFastClass {
-
-  public static FastClass create(Class type) {
-    return create(type.getClassLoader(), type);
-  }
-
-  public static FastClass create(ClassLoader loader, Class type) {
-    FastClass.Generator generator = new FastClass.Generator();
-    generator.setType(type);
-    generator.setClassLoader(loader);
-    generator.setNamingPolicy(new GuiceNamingPolicy());
-    return generator.create();
-  }
-}
diff --git a/src/com/google/inject/internal/GuiceNamingPolicy.java b/src/com/google/inject/internal/GuiceNamingPolicy.java
deleted file mode 100644
index c4d38ed..0000000
--- a/src/com/google/inject/internal/GuiceNamingPolicy.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (C) 2006 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.inject.internal;
-
-import net.sf.cglib.core.DefaultNamingPolicy;
-import net.sf.cglib.core.Predicate;
-
-/**
- * Cglib class naming policy for Guice.
- *
- * @author crazybob@google.com (Bob Lee)
- */
-public class GuiceNamingPolicy extends DefaultNamingPolicy {
-
-  @Override protected String getTag() {
-    return "ByGuice";
-  }
-}
diff --git a/test/com/google/inject/AllTests.java b/test/com/google/inject/AllTests.java
index 2c447d4..81added 100644
--- a/test/com/google/inject/AllTests.java
+++ b/test/com/google/inject/AllTests.java
@@ -19,6 +19,7 @@
 import com.google.inject.commands.CommandRecorderTest;
 import com.google.inject.commands.CommandReplayerTest;
 import com.google.inject.commands.CommandRewriteTest;
+import com.google.inject.internal.BytecodeGenTest;
 import com.google.inject.internal.FinalizableReferenceQueueTest;
 import com.google.inject.internal.LineNumbersTest;
 import com.google.inject.internal.ReferenceCacheTest;
@@ -84,6 +85,7 @@
     suite.addTestSuite(ReferenceMapTest.class);
     suite.addTestSuite(TypesTest.class);
     suite.addTestSuite(UniqueAnnotationsTest.class);
+    suite.addTestSuite(BytecodeGenTest.class);
 
     // matcher
     suite.addTestSuite(MatcherTest.class);
diff --git a/test/com/google/inject/internal/BytecodeGenTest.java b/test/com/google/inject/internal/BytecodeGenTest.java
new file mode 100644
index 0000000..a930f56
--- /dev/null
+++ b/test/com/google/inject/internal/BytecodeGenTest.java
@@ -0,0 +1,182 @@
+/**
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.inject.internal;
+
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Module;
+import static com.google.inject.matcher.Matchers.any;
+import java.io.File;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import junit.framework.TestCase;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+/**
+ * @author mcculls@gmail.com (Stuart McCulloch)
+ */
+public class BytecodeGenTest extends TestCase {
+
+  /**
+   * Custom URL classloader with basic visibility rules
+   */
+  static class TestVisibilityClassLoader
+      extends URLClassLoader {
+
+    public TestVisibilityClassLoader() {
+      super(new URL[0]);
+
+      final String[] classpath = System.getProperty("java.class.path").split(File.pathSeparator);
+      for (final String element : classpath) {
+        try {
+          // is it a remote/local URL?
+          addURL(new URL(element));
+        } catch (final MalformedURLException e1) {
+          try {
+            // nope - perhaps it's a filename?
+            addURL(new File(element).toURI().toURL());
+          } catch (final MalformedURLException e2) {
+            throw new RuntimeException(e1);
+          }
+        }
+      }
+    }
+
+    /**
+     * Classic parent-delegating classloaders are meant to override findClass.
+     * However, non-delegating classloaders (as used in OSGi) instead override
+     * loadClass to provide support for "class-space" separation.
+     */
+    @Override
+    protected Class<?> loadClass(final String name, final boolean resolve)
+        throws ClassNotFoundException {
+
+      if (name.startsWith("java.")) {
+
+        // standard bootdelegation of java.*
+        return super.loadClass(name, resolve);
+
+      } else if ((!name.contains(".internal.") && !name.contains(".cglib."))
+          || name.contains("Test")) {
+
+        /*
+         * load public and test classes directly from the classpath - we don't
+         * delegate to our parent because then the loaded classes would also be
+         * able to see private internal Guice classes, as they are also loaded
+         * by the parent classloader.
+         */
+        final Class<?> clazz = findClass(name);
+        if (resolve) {
+          resolveClass(clazz);
+        }
+        return clazz;
+      }
+
+      // hide internal non-test classes
+      throw new ClassNotFoundException();
+    }
+  }
+
+  interface ProxyTest {
+    String sayHello();
+  }
+
+  /**
+   * Note: this class must be marked as public or protected so that the Guice
+   * custom classloader will intercept it. Private and implementation classes
+   * are not intercepted by the custom classloader.
+   * 
+   * @see BytecodeGen#isHookable(Class)
+   */
+  protected static class ProxyTestImpl
+      implements ProxyTest {
+
+    public String sayHello() {
+      return "HELLO";
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testProxyClassLoading() {
+    final ClassLoader testClassLoader = new TestVisibilityClassLoader();
+
+    final Class<ProxyTest> testAPIClazz;
+    final Class<ProxyTest> testImplClazz;
+
+    try {
+      testAPIClazz = (Class<ProxyTest>) testClassLoader.loadClass(ProxyTest.class.getName());
+      testImplClazz = (Class<ProxyTest>) testClassLoader.loadClass(ProxyTestImpl.class.getName());
+    } catch (final Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    final ProxyTest test = Guice.createInjector(new Module() {
+      public void configure(Binder binder) {
+
+        binder.bind(testAPIClazz).to(testImplClazz);
+
+        // Force proxy...
+        binder.bindInterceptor(any(), any(), new MethodInterceptor() {
+          public Object invoke(MethodInvocation chain)
+              throws Throwable {
+            return chain.proceed() + " WORLD";
+          }
+        });
+      }
+    }).getInstance(testAPIClazz);
+
+    // verify method interception still works
+    assertEquals("HELLO WORLD", test.sayHello());
+  }
+
+  public void testProxyClassUnloading() {
+
+    ProxyTest testProxy = Guice.createInjector(new Module() {
+      public void configure(Binder binder) {
+        binder.bind(ProxyTest.class).to(ProxyTestImpl.class);
+        binder.bindInterceptor(any(), any(), new MethodInterceptor() {
+          public Object invoke(MethodInvocation chain)
+              throws Throwable {
+            return chain.proceed() + " WORLD";
+          }
+        });
+      }
+    }).getInstance(ProxyTest.class);
+
+    // take a weak reference to the generated proxy class
+    Reference<Class<?>> clazzRef = new WeakReference<Class<?>>(testProxy.getClass());
+
+    assertNotNull(clazzRef.get());
+
+    // null the proxy
+    testProxy = null;
+
+    /*
+     * this should be enough to queue the weak reference
+     * unless something is holding onto it accidentally.
+     */
+    System.gc();
+    System.gc();
+    System.gc();
+
+    assertNull(clazzRef.get());
+  }
+}