ClassLoaders are interesting and I’ve been playing around with them a lot lately and definitely needed to jot a few things down so that I could not only remember it all but also use if for future reference and allow everyone else to do the same.
ClassLoaders reference class definitions, not data. Class definitions are the actual code and reflection information about the Class and are completely separate from the data of a Class instance, which is stored on the heap. Class definitions are loaded by the ClassLoader and stored in a special heap. If a ClassLoader is ever loading a Class definition if first should delegate (and all the out-of-the-box ClassLoaders do this) to its parent. When its parent doesn’t find the Class definition, then ClassLoader is free to load it.
Okay, let’s say we have Class MyService
and Class Worker
. Class MyService
imports Class Worker
. We also have ClassLoader P
and ClassLoader C
. ClassLoader P
is the parent of ClassLoader C
(think C-hild and P-arent). In addition, the only way that this really makes any sense whatsoever is that the ClassLoader C
is the the current Thread context ClassLoader. There are a few situations that could occur:
- P loads MyService and P loads Worker
- P loads MyService and C loads Worker
- C loads MyService and P loads Worker
- C loads MyService and C loads Worker
Situation 1 means that the parent ClassLoader loads everything. The child ClassLoader doesn’t need to do anything. Situation 2 means that the parent ClassLoader loads MyService
and when it tries to also load Worker
it can’t. Since it is the parent, loading stops, but doesn’t fail because P
can’t ask “down” to C
for Worker
. Situation 3 means that C
loads MyService
and when it tries to also load Worker
, it first asks its parent for Worker
, which its parent has. Situation 4 means that C
loads both MyService
and Worker
. The parent is consulted twice, but has neither class.
Situation 2 is the sticky point here. Since the Sun JVM doesn’t fail during loading of MyService
, it is tough to know what is actually going on when things start failing. Let’s elaborate on 2 for a second. Supposed MyService
and Worker
look like this:
MyService.java ------ package com.inversoft; import com.inversoft.Worker; public class MyService { public void execute() { Worker worker = new Worker(); worker.doWork(); } } Worker.java ------ package com.inversoft; public class Worker { public void doWork() { System.out.println("Doing work"); } }
In this case and with the ClassLoader situation from 2, when MyService's
execute method is called, a ClassNotFoundException
will occur when Worker
is being instantiated. Even though C
is able to load Worker
AND it is the current context ClassLoader, it doesn’t matter! The golden rule of ClassLoaders, that always applies is that Classes from one ClassLoader cannot see Classes from child or sibling ClassLoaders. Since MyService
is loaded by ClassLoader P
, whenever it attempts to load other Classes it always starts with the ClassLoader it was loaded from. In this case, MyService
will ask P
for Worker
. Since P
doesn’t know about Worker
, a ClassNotFoundException
will be thrown.
Additionally, what this implies is that Class references are not bound at load time but rather delayed until they are referenced by the code that is executing. Likewise, Class definitions are bound to a ClassLoader and are constrained by that ClassLoader to only find Classes that the ClassLoader can resolve.
Must developers often refer to the fully-qualified name of Classes like:
com.inversoft.Worker
This is only paritally true. When most people discuss ClassLoaders and Classes in articles and papers they refer to the the Classes fully-qualified name like this:
("Worker", "com.inversoft", P)
This includes the name of the Class, the package the Class is defined in and the ClassLoader that loaded it. Therefore, when Class definitions are loaded into the JVM and stored onto the special Class definition heap, they are defined using these three pieces of information.
This situation is difficult to remedy. This tough situation becomes even more difficult when MyService
looks like this:
MyService.java ------ package com.inversoft; import com.inversoft.Worker; public class MyService extends Thread { public void run() { Worker worker = new Worker(); worker.doWork(); } }
And MyService has a client that looks like this:
Client.java ----------- package com.inversoft; public class Client { public void client() { ClassLoader p = Thread.currentThread().getContextClassLoader(); ClassLoader c = makeClassloader(p); MyService service = new MyService(); service.setContextClassLoader(c); service.start(); } }
In this situtation, the Thread that is executing is also the Thread whose context ClassLoader has been set and it is also the service itself. Since the Thread was loaded by P
, it makes little difference that I’ve changed the context ClassLoader to C
. This code will still fail. So, here’s how I solved this problem.
First I realized that fully-qualified Class definitions don’t mean that the invocation of the class itself isn’t dynamic at runtime. The reason being that instances of Classes loaded by a ClassLoader can be passed around freely to instances of Classes loaded into any other ClassLoader. This is allowed only when the Class extends another Class or implements an Interface available in the other ClassLoaders (specifically because of situation 2 from above). Java holds instances in a shared pool that can be shared across Threads and have little concept of the ClassLoader that loaded them (except a reference to it and as part of its Class definition). In order to fully understand this, we need more examples. Let’s assume one more Class, AbstractWorker
, from which Worker
extends.
AbstractWorker.java -------------- package com.inversoft; public class AbstractWorker { public abstract void doWork(); } Worker.java ------ package com.inversoft; public class Worker extends AbstractWorker { // Same as before }
We also have to change MyService
and the client so that they uses AbstractWorker
.
MyService.java ------ package com.inversoft; import com.inversoft.AbstractWorker; public class MyService extends Thread { AbstractWorker worker; public MyService(AbstractWorker worker) { this.worker = worker; } public void run() { worker.doWork(); } } Client.java ----------- package com.inversoft; public class Client { public void client() { ClassLoader p = Thread.currentThread().getContextClassLoader(); ClassLoader c = makeClassloader(p); // P doesn't know about Worker, so we have to ask C to instantiate it Class bClass = c.loadClass("com.inversoft.Worker"); MyService service = new MyService(bClass.newInstance()); service.setContextClassLoader(c); service.start(); } }
Now, if we walk back over situation 2 from above, we can see what happens. First C
loads Class Worker
. Next, MyService
is loaded by P
, and passed Worker
. During this instantiation and while the constructor is executing, MyService
needs to load up AbstractWorker
(recall that the Sun JVM performs lazy loading of Class definitions). Here, since MyService
was loaded by P
, when AbstractWorker
needs to be loaded, it asks P
to load it. Here we have two more situations:
- 2.0 P loads AbstractWorker
- 2.1 C loads AbstractWorker
In situation 2.0, P
loads AbstractWorker
, which makes MyService
fully functional because it has all the Classes it needs. In situation 2.1, P
still cannot load AbstractWorker
and we are stuck in the same situation where P
will always error out when it attempts to use AbstractWorker
. In this new situation the error will occur when the MyService
is initialized. The important point is that situation 2.0 works fine when P
attempts to load the AbstractWorker
Class definition.
Now what happens when the Client
starts the MyService
thread is that the instance of Worker
is created by loading the Class definition for Worker
using C
. Worker
extends AbstractWorker
, which is all that MyService
knows about. When MyService
is handed a pointer to Worker
it uses it as an AbstractWorker
, but the Class that is running is the Worker
instance. This is the crux of ClassLoader usage. Since Worker
was already loaded, it doesn’t matter that it was loaded by another ClassLoader, MyService
is still free to makes calls to Worker
via its super-class AbstractWorker
.
This illustrates that instances of Objects aren’t ClassLoader bound, while the Class deinitions are. Instances are free to roam about the JVM across Thread boundaries and to Classes loaded by any ClassLoader as long as the Class definitions of the Object instances are either known by the ClassLoader in question (situation 1, 3 and 4) or known via a super-class or interface (situation 2.0). In all other cases, the ClassLoader does not have enough information at at runtime to completely setup the Class definition and therefore will failed at runtime, usually with a ClassNotFoundException
.