Java Institute DreamsCity 2000
Welcome to DreamsCity
Return to Java Institute

Class Loaders mechanism

written by Stuart Halloway
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:

         * Class loaders as a namespace mechanism
         * Relating class loaders to the class path
         * Using class loaders for hot deployment       
                  
These tips were developed using Java(tm) 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 Java(tm) 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 Java(tm) Virtual machine* 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 JDK 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 
loadNewVerionOfServer(), 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:

  A rolling stone gathers no moss

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:

- Book: "Inside The Java Virtual Machine," by Bill Venners.

- Article: "A New Era for Java Protocol Handlers," by Brian Maso
  http://java.sun.com/jdc/onlineTraining/protocolhandlers/
  
- White paper: "Understand Class.forName()", by Ted Neward
  http://www.javageeks.com/Papers/ClassForName

Any comments? email to:
richard@dreamscity.net