-
Bug
-
Resolution: Unresolved
-
P4
-
None
-
1.4.0
-
x86
-
linux
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
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