JDK-5102804 : Memory leak in Introspector.getBeanInfo(Class) for custom BeanInfo: Class param
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.beans
  • Affected Version: 5.0u23-rev,6
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: linux_redhat_9.0,windows
  • CPU: x86
  • Submitted: 2004-09-15
  • Updated: 2010-07-02
  • Resolved: 2009-12-16
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other Other Other JDK 6 JDK 7
5.0u23-revFixed 5.0u24-revFixed 5.0u25Fixed 6u18-revFixed 7 b78Fixed
Related Reports
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Relates :  
Description
Run the following test program:

---%<---
package testintrospectorleak;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class Main {
    public static void main(String[] args) throws Exception {
        ClassLoader l = new L();
        Class c = Class.forName("testintrospectorleak.Main$B", true, l);
        if (c.getClassLoader() != l) throw new Error("Wrong loader for B!");
        l = null; // release ref
        BeanInfo bi = Introspector.getBeanInfo(c);
        System.out.println("Got BeanInfo " + bi + " for " + c + " with defaultPropertyIndex " + bi.getDefaultPropertyIndex());
        bi = null; // release ref
        Reference r = new WeakReference(c);
        c = null; // release ref
        System.out.println("Class collected? " + collectible(r));
        Introspector.flushCaches();
        System.out.println("Class collected after I.fC? " + collectible(r));
    }
    // Try as hard as possible to collect a reference, even if soft.
    // Copied from org.netbeans.junit.NbTestCase.assertGC.
    private static boolean collectible(Reference ref) {
        List alloc = new ArrayList();
        int size = 100000;
        for (int i = 0; i < 50; i++) {
            if (ref.get() == null) {
                return true;
            }
            System.gc();
            System.runFinalization();
            try {
                alloc.add(new byte[size]);
                size = (int) (((double) size) * 1.3);
            } catch (OutOfMemoryError error) {
                size = size / 2;
            }
            try {
                if (i % 3 == 0) {
                    Thread.sleep(321);
                }
            } catch (InterruptedException t) {}
        }
        return false;
    }
    // Class loader which specifically loads B by itself (does not delegate).
    // Could also load it from a different code source, but this is easier to set up.
    private static final class L extends URLClassLoader {
        public L() {
            super(new URL[] {Main.class.getProtectionDomain().getCodeSource().getLocation()});
        }
        protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
            Class c = findLoadedClass(name);
            if (c == null) {
                if (!name.equals("testintrospectorleak.Main$B") && !name.equals("testintrospectorleak.Main$BBeanInfo")) {
                    try {
                        c = getParent().loadClass(name);
                    } catch (ClassNotFoundException e) {
                        c = findClass(name);
                    }
                } else {
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    // A simple bean. Loaded from L, not main app class loader.
    public static final class B {
        public B() {}
        public int getX() {return 0;}
        public void setX(int x) {}
    }
    // Its BeanInfo. Again loaded only from L.
    public static final class BBeanInfo extends SimpleBeanInfo {
        public BBeanInfo() {}
        public int getDefaultPropertyIndex() {
            return 0;
        }
        public PropertyDescriptor[] getPropertyDescriptors() {
            try {
                return new PropertyDescriptor[] {
                    new PropertyDescriptor("x", Class.forName("testintrospectorleak.Main$B")),
                };
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
}
---%<---

Under either JDK 1.4.2_04-b02 or 1.5.0-rc-b63 on Linux, the results are similar:

---%<---
Got BeanInfo java.beans.GenericBeanInfo@757aef for class testintrospectorleak.Main$B with defaultPropertyIndex 0
Class collected? false
Class collected after I.fC? true
---%<---

If you comment out BBeanInfo, recompile (taking care to delete the old Main$BBeanInfo.class!), and run it again, then it works as desired on JDK 1.5:

---%<---
Got BeanInfo java.beans.GenericBeanInfo@757aef for class testintrospectorleak.Main$B with defaultPropertyIndex -1
Class collected? true
Class collected after I.fC? true
---%<---

though not on JDK 1.4.2:

---%<---
Got BeanInfo java.beans.GenericBeanInfo@1be2d65 for class testintrospectorleak.Main$B with defaultPropertyIndex -1
Class collected? false
Class collected after I.fC? true
---%<---

Meaning that the basic case - no custom BeanInfo - was fixed in JDK 1.5, but not the case where there is custom BeanInfo.

Interpretation of results: Introspector's cache (beanInfoCache) uses strong refs as values, meaning that GenericBeanInfo instances are held strongly. With no custom BeanInfo, it seems that all of the GBI's fields refer at most softly to the original Class. However if there is a custom BeanInfo, GBI seems to hold this strongly in the targetBeanInfo field. In that case, the fact that bIC is a WeakHashMap is irrelevant; the key cannot be collected since the value holds it strongly anyway.

Why is this relevant? All sorts of code can call Introspector.getBeanInfo for a variety of reasons, it is pretty common. If some class is loaded from a temporary class loader (e.g. an Ant <taskdef> with nested <classpath>, but many other examples are possible), and it has a specific BeanInfo loaded by I.gBI, then the class will not be collectible. This means the ClassLoader is not collectible either, nor any of the other classes loaded by it, nor their static fields, nor any parent class loaders, etc. This can cause a huge memory leak in some cases. (And on Windows could result in a JAR file lock being held open for the life of the VM.) In particular, it is thought to be the root cause of the NetBeans IDE memory leak bug described at

http://www.netbeans.org/issues/show_bug.cgi?id=46532

Petr Nejedly claims that the bug is reproducible in the stock JDK using an unpatched NetBeans but that if Introspector's cache is patched to not refer strongly to values, it is not reproducible.
###@###.### 2004-09-15

Comments
EVALUATION WeakHashMap holds reference as a value.
26-11-2009

EVALUATION Reproducible as described w/ 1.5, 1.4.2, & 1.4.1, though not with 1.4.0_04. Perhaps some 1.4.1 change caused this to start happening. In any case, this should be fixed. ###@###.### 2004-09-20 Description gives a good investigation on what is happening. However, we need to carefully examine the suggested solution - it could be imperfect. ###@###.### 2005-06-06 15:24:02 GMT
06-06-2005

CONVERTED DATA BugTraq+ Release Management Values COMMIT TO FIX: mustang
22-09-2004

WORK AROUND Call Introspector.flushCaches() whenever in doubt, contrary to its Javadoc which claims "This method is not normally required. It is normally only needed by advanced tools that update existing "Class" objects in-place and need to make the Introspector re-analyze existing Class objects." ###@###.### 2004-09-15
15-09-2004

SUGGESTED FIX Perhaps make Introspector.GenericBeanInfo hold targetBeanInfo as a SoftReference. ###@###.### 2004-09-15
15-09-2004