WELCOME to the Java Developer ConnectionSM (JDC) Tech Tips, October 31, 2000.
This issue is about class loaders
. When
a Java program doesn't load successfully, developers usually
suspect the class path. In reality, the class path is a special
case of a powerful class loading architecture built around the
java.lang.ClassLoader
class. This tip covers:
This issue of the JDC Tech Tips is written by Stuart Halloway, a
Java specialist at DevelopMentor
(http://www.develop.com/java).
These tips were developed using JavaTM 2 SDK,
Standard Edition, v 1.3.
CLASS LOADERS AS A NAMESPACE MECHANISM
Program developers must cope with the problem of name collisions.
This is true for all programming environments. If you name a class
BankAccount
there is a good chance that somebody else will
use the same name for another class. Sooner or later the two
classes will collide in the same process, wreaking havoc.
Programmers who begin to use the JavaTM
programming language learn to use packages to prevent name collisions.
Instead of naming a class BankAccount
you place the class in a package, perhaps
naming the class something like com.develop.bank.BankAccount
(that
is, the reverse of your domain name). Hopefully there is minimal
danger of a name collision with this approach.
However with a language as dynamic as the Java programming
language, "minimal danger" is not safe enough. The package naming
scheme relies on the cooperation of all developers in a system.
This is difficult to coordinate and is error-prone. Imagine, for
example, the confusion that would result if the European and
American branches of a company each created a
com.someco.FootballPlayer
class! More importantly, Java
applications can run for a long time--long enough that you might
recompile and ship a new version of a class without ever shutting
down the application. This leads to multiple versions of the same
class trying to live in the same application.
The JavaTM Virtual machine1 handles these problems through its
class loader architecture. Every class in an application is loaded
by an associated ClassLoader
object. How does this solve the name
collision problem? The VM treats classes loaded by different
class loaders as entirely different types, even if their packages
and names are exactly the same. Here's a simple example:
import java.net.*;
public class Loader {
public static void main(String [] args)
throws Exception
{
URL[] urlsToLoadFrom =
new URL[]{new URL(
"file:subdir/")};
URLClassLoader loader1 =
new URLClassLoader(urlsToLoadFrom);
URLClassLoader loader2 =
new URLClassLoader(urlsToLoadFrom);
Class cls1 =
Class.forName("Loadee",
true, loader1);
Class cls2 =
Class.forName("Loadee",
true, loader2);
System.out.println("(cls1 == cls2)
is "
+ ((cls1 == cls2) ? "true" :
"false"));
}
}
//place Loadee in a subdir named 'subdir'
public class Loadee {
static {
System.out.println("Loadee class
loaded");
}
}
Compile the Loader class
, then compile the Loadee class
in a
subdirectory named "subdir." When you run the Loader class, you
should see the output:
Loadee class loaded
Loadee class loaded
(cls1 == cls2) is false
Both cls1 and cls2
are named Loadee
. In fact, they both come from
the same .class file (although that does not need to be the
case, in general). Nevertheless, the VM treats cls1
and cls2
as two
separate classes.
RELATING CLASS LOADERS TO THE CLASS PATH
The example above is interesting, but most developers do not code
that way. Instead of using the reflection method Class.forName()
to load classes into the VM, they simply write code such as:
// load a Foo (don't bother me about
// where it came from...)
Foo f = new Foo();
In order for this to compile and run, the Foo
class
must be in the class path (or in a few other special locations beyond the scope
of this tip). The class path is the place where most developers
interact with class loaders, although implicitly. But from the
perspective of the VM, the class path is just a special case of
the class loader architecture.
When a Java application starts, it creates a class loader that
searches a set of URLs. The application initializes the class
loader to use file URLs based on the values in the class path.
Assuming that the application's main class is in the class path,
the main class is loaded and begins executing. After that, class
loading is implicit. Whenever a class refers to another class, for
example, by initializing a field or local variable, the referent
is immediately loaded by the same class loader that loaded the
referencing class. Here's an example:
// compile these classes all in one
// file ReferencingClass.java
class Referent {
static {
System.out.println("Referent
loaded by " +
Referent.class.getClassLoader());
}
}
public class ReferencingClass {
public static void main(String [] args) {
System.out.println("ReferencingClass
loaded by " +
ReferencingClass.class.getClassLoader());
//refer to Referent
new Referent();
System.out.println("String loaded by
" +
String.class.getClassLoader());
}
}
When you compile and run the ReferencingClass
, you should see
that both ReferencingClass
and Referent
are loaded by the same
class loader. (If you are using the JDKTM it will be a nested class
of sun.misc.Launcher
.) However, the String
class is loaded by a
different class loader. In fact the String
class is loaded by the
"null" class loader, even though String
is also referenced by
ReferencingClass
. This is an example of class loader delegation.
A class loader has a parent class loader, and the set of a class
loader and its ancestors is called a delegation.
Whenever a class loader loads a class, it must consult its parent
first. A standard Java application begins with a delegation of
three class loaders: the system class loader, the extension class
loader, and the bootstrap class loader. The system class loader
loads classes from the class path, and delegates to the extension
class loader, which loads Java extensions. The parent of the
extension class loader is the the bootstrap class loader, also
known as the null class loader. The bootstrap class loader loads
the core API.
The delegation model has three important benefits. First, it
protects the core API. Application-defined class loaders are not
able to load new versions of the core API classes because they
must eventually delegate to the boostrap loader. This prevents the
accidental or malicious loading of system classes that might
corrupt or compromise the security of the VM. Second, the
delegation model makes it easy to place common classes in a shared
location. For example, in a servlet engine the servlet API classes
could be placed in the class path where they can be shared. But
the actual servlet implementations might be loaded by a separate
URLClassLoader
so that they can be reloaded later. Third, the
delegation model makes it possible for objects loaded by different
class loaders to refer to each other through superclasses or
superinterfaces that are loaded by a shared class loader higher in
the delegation.
USING CLASS LOADERS FOR HOT DEPLOYMENT
The ability to load mutiple classes with the same name into the
virtual machine allows servers to partition processing into
separate namespaces. This partitioning could be space-based,
separating code from different sources to simplify security. For
example, applets from two different codebases could run in the
same browser process. Or, the partitioning could be time-based.
Here, new versions of a class could be loaded as they become
available. This time-based partitioning feature is sometimes
known as hot deployment. The following code demonstrates hot
deployment in action.
//file ServerItf.java
public interface ServerItf {
public String getQuote();
}
//file Client.java
import java.net.URL;
import java.net.URLClassLoader;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Client {
static ClassLoader cl;
static ServerItf server;
public static void loadNewVersionOfServer()
throws Exception {
URL[] serverURLs =
new URL[]{new URL("
file:server/")};
cl = new URLClassLoader(serverURLs);
server = (ServerItf) cl.loadClass(
"ServerImpl").newInstance();
}
public static void test() throws Exception {
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
loadNewVersionOfServer();
while (true) {
System.out.print(
"Enter QUOTE, RELOAD, GC, or
QUIT: ");
String cmdRead = br.readLine();
String cmd = cmdRead.toUpperCase();
if (cmd.equals("QUIT")) {
return;
} else if (cmd.equals("
QUOTE")) {
System.out.println(
server.getQuote());
} else if (cmd.equals("
RELOAD")) {
loadNewVersionOfServer();
} else if (cmd.equals("
GC")) {
System.gc();
System.runFinalization();
}
}
}
public static void main(String [] args) {
try {
test();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
// file ServerImpl.java. Place this file
// in a subdirectory named 'server'.
class Reporter {
Class cls;
Reporter(Class cls) {
this.cls = cls;
System.out.println(
"ServerImpl class " +
cls.hashCode() +
" loaded into VM");
}
protected void finalize() {
System.out.println("ServerImpl
class " +
cls.hashCode() + " unloaded
from VM");
}
}
public class ServerImpl implements ServerItf {
//catch the class being unloaded from the VM
static Object reporter =
new Reporter(ServerImpl.class);
public String getQuote() {
return "A rolling stone gathers no
moss";
}
}
Compile the Client
and ServerItf
files in a directory, and then
compile the ServerImpl
in a subdirectory named "server." Make
sure to include the higher level directory on your class path
when you compile ServerImpl
, for example:
javac -classpath \mypath\ServerImpl.java
When you start the Client
, you should see the following prompt:
Enter QUOTE, RELOAD, GC, or QUIT:
Enter QUOTE to see the current quote from the server:
A rolling stone gathers no moss
Now, without shutting down the process, use another console or GUI
to edit the ServerImpl
class. Change the getQuote
method to return
a different quote, for example, "Wet birds do not fly at night."
Recompile the server class. Then return to the console where the
Client
is still running and enter RELOAD. This invokes the method
loadNewVersionOfServer()
, which uses a new instance of
URLClassLoader
to load a new version of the server class. You
should see something like this:
ServerImpl class 7434986 loaded into VM
Reissue the QUOTE command. You should now see your new version of
the quote, for example:
Wet birds do not fly at night
Notice that you did this without shutting down your application.
This same technique is used by servlet engines such as Apache
Software Foundation's Tomcat to automatically reload servlets
that have changed.
There are a few interesting points about explicitly using class
loaders in your application. First, instances of Class
and
ClassLoader
are simply Java objects, subject to the normal memory
rules of the Java(tm) platform. In other words, when classes and
class loaders are no longer referenced, they can be reclaimed by
the garbage collector. This is important in a long-running
application, where unused old versions of classes could waste
a lot of memory. However, making sure that classes are
unreferenced can be tricky. Every instance has a reference to its
associated class, every class has a reference to its class loader,
and every class loader has a reference to every class it ever
loaded. It's easy to view this tangled knot of references as a
class loader "hairball." If you have a reference to any object in
the hairball, none of the objects can be reclaimed by the garbage
collector. In the simple Client application above, you can verify
by inspection that all references to the old class loader and its
classes are explicitly dropped when a new class loader is created.
If you need more proof, you can issue the GC command from the
Client console. On a VM with a reasonably aggressive GC
implementation, you should see a log message indicating that the
old ServerImpl
class has been reclaimed by the garbage collector.
Notice that the Client
code never refers to ServerImpl
directly.
Instead, the ServerImpl
instance is held in a reference of type
ServerItf
. This is critical to making explicit use of class
loaders. Remember the rules about implicit class loading, and
imagine what would happen if the Client
had a field of type
ServerImpl
. When the VM needs to initialize that field, it uses
the Client
's class loader to try to load ServerImpl
. Client
is the
application main class, so it is loaded from the class path by the
system class loader. Because the ServerImpl
class is not on the
class path, the reference to it causes a NoClassDefFoundError
.
Don't be tempted to "fix" this by placing ServerImpl
in the class
path. If you do that, the ServerImpl
class will indeed load, but
it will load under control of the system class loader. This
defeats hot deployment because your instances of URLClassLoader
delegate to the system class loader. So, no matter how many times
you create a URLClassLoader
, you always get the copy of ServerImpl
that was originally loaded from the class path.
For more information on class loaders, see:
Note
Sun respects your online time and privacy. The Java Developer
Connection mailing lists are used for internal Sun MicrosystemsTM purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page
(http://developer.java.sun.com/subscription/), uncheck the
appropriate checkbox, and click the Update button.
Subscribe
To subscribe to a JDC newsletter mailing list, go to the
Subscriptions page (http://developer.java.sun.com/subscription/), choose the newsletters you want to subscribe to, and click Update.
Feedback
Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com
Archives
You'll find the JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
Copyright
Copyright 2000 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This Document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
JDC Tech Tips
October 31, 2000
_______
1 As used in this document, the terms "Java virtual machine"
or "JVM" mean a virtual machine for the Java platform.