Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-5102804

Memory leak in Introspector.getBeanInfo(Class) for custom BeanInfo: Class param

XMLWordPrintable

    • b78
    • x86
    • linux_redhat_9.0, windows

        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

              malenkov Sergey Malenkov (Inactive)
              jglick Jesse Glick (Inactive)
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

                Created:
                Updated:
                Resolved:
                Imported:
                Indexed: