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

UIDefaults too aggressive re. caching of Class instances; gets classloader wrong

XMLWordPrintable

      Name: gm110360 Date: 04/26/2002


      FULL PRODUCT VERSION :
      java version "1.4.0-beta3"
      Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.0-beta3-b84)
      Java HotSpot(TM) Client VM (build 1.4.0-beta3-b84, mixed mode)

      Problem also occurs using 1.3.1_02.

      glibc-2.1.3-21
      Linux jesse_laptop.netbeans.com 2.2.12-20 #1 Mon Sep 27
      10:40:35 EDT 1999 i686 unknown
      Red Hat Linux release 6.1 (Cartman)


      A DESCRIPTION OF THE PROBLEM :
      Some pieces of code in javax.swing.UIDefaults cache live
      metadata objects, i.e. Class's and things in
      java.lang.reflect.*, in order to speed up the search for
      component UIs. Two things I know of are:

      1. getUIClass caches loaded classes, as the values in the
      map where the key is the class name.

      2. getUI caches createUI(JComponent) Method's, as values
      where the key is the Class providing the factory method.

      UIDefaults uses a simple cache: it holds hard references and
      makes no attempt to clear the cache. This is bad. Consider
      what happens when the component (specifically, its PLAF
      class) is loaded in a dynamically constructed classloader,
      i.e. not from the application classpath. It will work;
      UIDefaults knows to ask for the correct ClassLoader.
      However, if you then discard this classloader and create a
      new one loading essentially the same code, and try to use
      the component again, generally it will fail terribly.
      UIDefaults will try to get a UI from the old ComponentUI
      subclass, which will probably result in a ClassCastException
      when the component tries to use it. (In fact the real error
      will not be reported at all unless you patch
      UIDefaults.java, but I have filed that as a separate bug.)

      Observed in the NetBeans (= Forte for Java) IDE when trying
      to reload the JavaHelp library as part of a dynamically
      loaded module. The first time it is loaded, all seems well.
      But if you reload the module including the library,
      mysterious errors are thrown whenever any attempt to make a
      JavaHelp Swing component is made.

      Recommendations:

      1. UIDefaults (or for that matter anything like it) should
      never hold a hard reference to a Class or anything based on
      a Class such as a Method. For one thing, it is a memory leak
      that can prevent the ClassLoader from being garbage
      collected, which in turn can prevent the JAR file from which
      classes were loaded from being unlocked on Windows, and so
      on. Only weak references should be used. If the reference
      breaks, recalculate.

      2. A check should be made that the ClassLoader from the
      cached Class matches the ClassLoader that was actually
      requested, if this is possible. That would prevent
      ClassCastException's from occurring. Since a component might
      in principle load PLAF classes from a parent classloader,
      this could be a bit subtle; i.e. if getUI(A1) is called
      once, where A1 is loaded from loader L1 whose parent is L0,
      but the UI class U0 is loaded from L0, and getUI(A2) is
      later called where A2 is from L2 extends L0, it is fine to
      reuse U0 - it still applies. In the more typical case, A1's
      UI class is U1 from L1, A2's is U2 from L2, and U2 cannot be
      substituted for U1. So probably the check should be this: if
      the classloader of the UI class is the same as, or an
      ancestor of, the classloader of the queried component class,
      use it; else recalculate it. I.e. U0 is fine since it is
      loaded from L0, an ancestor of L1 and L2 alike; U1 is not
      fine when you are querying for A2, since L1 is not an
      ancestor of L2. Note that this is a heuristic, not a perfect
      solution, as some pathological cases could trick it; the
      perfect case is to always ask the loader of the component
      class for the UI class, i.e. not cache it at all, but that
      is unacceptable for performance.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Compile and run the attached class.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      Expected results:

      ---%<---
      Updating UI of an instance of c1...
      Updating UI of an instance of c2...
      ---%<---

      Actual results:

      ---%<---
      Updating UI of an instance of c1...
      Updating UI of an instance of c2...
      java.lang.ClassCastException: testuidef.TestUIDefReload$MyUI
      at
      testuidef.TestUIDefReload$FooComp.updateUI(TestUIDefReload.java:24)
      at testuidef.TestUIDefReload.main(TestUIDefReload.java:14)
      ---%<---


      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      package testuidef;
      import javax.swing.*;
      import javax.swing.plaf.ComponentUI;
      import java.net.*;
      public abstract class TestUIDefReload {
          public static void main(String[] x) throws Exception {
              UIDefaults d = UIManager.getDefaults();
              d.put("MyUI", "testuidef.TestUIDefReload$ImplMyUI");
              Class c1 = new XLoader().loadClass("testuidef.TestUIDefReload$FooComp");
              System.err.println("Updating UI of an instance of c1...");
              ((JComponent)c1.newInstance()).updateUI();
              Class c2 = new XLoader().loadClass("testuidef.TestUIDefReload$FooComp");
              System.err.println("Updating UI of an instance of c2...");
              ((JComponent)c2.newInstance()).updateUI();
          }
          private static class XLoader extends URLClassLoader {
              public XLoader() {
                  super(new URL[]
      {TestUIDefReload.class.getProtectionDomain().getCodeSource().getLocation()}, null);
              }
          }
          public static class FooComp extends JComponent {
              public void updateUI() {
                  try {
                      setUI((MyUI)UIManager.getUI(this));
                  } catch (RuntimeException re) {
                      // UIDefaults will not print this on its own,
                      // so catch it here for illustration:
                      re.printStackTrace();
                  }
              }
              public String getUIClassID() { return "MyUI"; }
          }
          public static class MyUI extends ComponentUI {}
          public static class ImplMyUI extends MyUI {
              public static ComponentUI createUI(JComponent c) {
                  return new MyUI();
              }
          }
      }

      ---------- END SOURCE ----------

      CUSTOMER WORKAROUND :
      Workarounds are possible, depending on the scenario. In the
      case of the NetBeans module including JavaHelp, there is a
      programmatic hook called just before the module is
      destroyed, which can be used to clean up UIDefaults' bad
      cache manually:

          public void uninstalled() {
              // other stuff, then...
              cleanDefaults(UIManager.getDefaults());
              // just to be safe, not actually needed:
              cleanDefaults(UIManager.getLookAndFeelDefaults());
          }
          private static void cleanDefaults(UIDefaults d) {
              Set badKeys = new HashSet(10); // Set<Object>
              Iterator it = d.entrySet().iterator();
              ClassLoader aboutToDie =
      ThisClass.class.getClassLoader();
              while (it.hasNext()) {
                  Map.Entry e = (Map.Entry)it.next();
                  Object k = e.getKey();
                  Object o = e.getValue();
                  if (o instanceof Class) {
                      // String -> Class loading cache
                      Class c = (Class)o;
                      if (c.getClassLoader() == aboutToDie) {
                          badKeys.add(k);
                      }
                  } else if (k instanceof Class) {
                      // Class -> Method createUI cache
                      Class c = (Class)k;
                      if (c.getClassLoader() == aboutToDie) {
                          badKeys.add(k);
                      }
                  }
              }
              it = badKeys.iterator();
              while (it.hasNext()) {
                  d.put(it.next(), null);
              }
          }
       ^E

      The workaround seems to work as desired, though I have only
      tested it lightly: you can create JavaHelp viewers even
      after reloading in a new classloader.
      (Review ID: 138334)
      ======================================================================
      ###@###.### 10/13/04 20:23 GMT

            Unassigned Unassigned
            gmanwanisunw Girish Manwani (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

              Created:
              Updated:
              Imported:
              Indexed: