类加载到虚拟机当中需要通过该类的ClassLoader完成,通常的ClassLoader有三种:bootStrap classLoader、extClassLaoder、appClassLoader。除此之外还可以由开发者自定义ClassLoader。
ClassLoader以何种规则加载Class呢?java2/jdk1.2推出了“委派双亲”模型(delegating parent model)。该模型的主要思想是,所有这些classLoader形成一条继承链,链条中一个节点代表一个ClassLoader,上一个节点是下一个节点的“parent”,当在A类中加载B类时,加载A类的ClassLoader(称为ClassLoader-A)把加载B类的任务委托给ClassLoader-A的父节点ClassLoader-A1,ClassLoader-A1又委托给它自己的父节点……直到bootStrap classLoader,即链条顶端。若bootStrap加载成功便罢,否则传给它的子节点,如此这般直到ClassLoader-A,若ClassLoader-A依然不能加载则宣判失败,抛出异常ClassNotFoundException。
链条前端的三个节点便是bootStrap、extClassLoader、appClassLoader。用以下代码验证之:










输出结果:
class sun.misc.Launcher$AppClassLoader
class sun.misc.Launcher$ExtClassLoader
可见加载MyClass的加载器便是appClassLoader,其父自然是ExtClassLoader,祖父bootStrap却不见踪影,变成null,何故?bootStrap并非是pure java,乃是用C语言(或许还有汇编?)写成,而ext、app二者却是java所写,因此bootStrap这位幕后英雄便不得以java object的身份示人吧?
那么这三种ClassLoader分别用来加载哪些class呢?依然以代码判断:
public static void main(String[] args) {
System.out.println("bootStrap??:" + System.getProperty("sun.boot.class.path"));
System.out.println("extClassLoader??:" + System.getProperty("java.ext.dirs"));
System.out.println("appClassLoader??:" + System.getProperty("java.class.path"));
}
Jdk把信息都放到了System所辖的properties当中了,分别以“sun.boot.class.path”、“java.ext.dirs”、“java.class.path”为key自可取出,整理得到如下结果:
ClassLoader
|
加载的class路径
|
bootStrap
|
C:/Program Files/Java/jre1.6.0_01/lib/resources.jar;
C:/Program Files/Java/jre1.6.0_01/lib/rt.jar;
C:/Program Files/Java/jre1.6.0_01/lib/sunrsasign.jar;
C:/Program Files/Java/jre1.6.0_01/lib/jsse.jar;
C:/Program Files/Java/jre1.6.0_01/lib/jce.jar;
C:/Program Files/Java/jre1.6.0_01/lib/charsets.jar;
C:/Program Files/Java/jre1.6.0_01/classes
|
extClassLoader
|
C:/Program Files/Java/jre1.6.0_01/lib/ext;
C:/WINDOWS/Sun/Java/lib/ext
|
appClassLoader
|
.
|
可以看到bootStrap用于加载jvm的核心类库,extClassLoader加载的类是相对固定的对核心类的扩展,appClassLoader则加载应用程序类。我若在shell中用set classpath重新设定应用路径自可改变表格里appClassLoader的结果。
下面开始验证“delegating parent”机制并引出压轴大戏context classLoader。定义两个类MyClass、MyOther:































































(1)在shell中以javac编译,在源码目录(设为${classpath})下得到MyClass.class和MyOther.class,并以“java MyClass”运行,得到结果:
$$$Something in MyOther:my_arg
(2)我把MyOther.class扔到${jre}/lib/ext/classes(设为${ext-path})当中,在${classpath}运行“java MyClass”结果仍然正确。
(3)现在我把MyClass.class也扔到${ext-path}中,并在${ext-path}中运行“java MyClass”,结果当然还是正确的。
(4)最后我把MyOther.class放回${classpath},在shell中设定classpath时加上${classpath},并在${ext-path}中运行MyClass,却得到:
java.lang.ClassNotFoundException: MyOther
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClassInternal(Unknown Source)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Unknown Source)
at MyClass.func(MyClass.java:9)
at MyClass.main(MyClass.java:84)
两个 class在同一目录下的情形如(1)、(3)都容易理解,而(2)和(4)则正好验证了“delegating parent”模型。${ext-path}中的class(直接在目录中或者在jar中)正是由extClassLoader来加载,而${classpath}中的class是由appClassLoader加载,其中appClassLoader也叫做System ClassLoader。前面说过extClassLoader是appClassLoader的“parent”。
在这四种情形中,当两个class在同一目录下时会由同一个classLoader加载,因为当前classLoader的祖祖辈辈都找不到MyOther,正好由当前MyClass的类加载器完成对MyOther的加载。
当MyClass在${class-path}中而MyOther在${ ext-path }中时,app-ClassLoader把加载委托给ext-classLoader,后者在委托给bootStrap,bootStrap对此无所作为又还给ext-classLoader,ext-classLoader正好在其辖区找到MyOther,于是乎大功告成。
反之,当MyClass在${ ext-path }当中时,就会由加载MyClass的ext-ClassLoader去加载MyOther,而MyOther却在${classpath}当中,ext-ClassLoader以及其父bootStrap均束手无策,只好告知“ClassNotFoundException”。
这一束手无策就带来了一个问题。当开发诸如jdbc这样的应用时,jdk总是提供一组接口,具体driver的实现则开放给别人。这样接口保持不变并且和具体的jdbc-driver解除了耦合,增强了灵活性。问题在于接口是在jdk层次提供的,即由bootStrap或者extClassLoader加载,而实现类却是由appClassLoader加载,因此在“delegating parent model”下接口调用实现时是找不到实现类的,只有灰头土脸的说“ClassNotFoundException”了。
解决方案就着落在context ClassLoader上,将MyClass代码改为:














如此编译后再把MyClass放到${ext-path},把MyOther放到${classpath},在shell中设定好classpath后在${ext-path}中运行MyClass,结果正确。
每个线程都有一个context classLoader用于加载类和资源,如果该线程未设定就由该线程的父线程来提供。应用程序初始化时当ExtClassLoader和AppClassLoader创建完毕后线程的context ClassLoader就会设为AppClassLoader,因此上述代码从线程上下文中找出AppClassLoader再从${class-path}中就不难找到MyOther,问题解决。
另外一个办法是调用Class.forName的另一个重载版本public static
Class,在其中指定classLoader。 forName(
String name, boolean initialize,
ClassLoader loader)