1 问题背景
我们都知道,Java平台一大亮点就在于其类装载器体系结构,这使得JVM可以在运行期从Java API,扩展路经(java.ext.path),classpath以及用户指定的位置(文件或网络)中载入所需的class,从而达到动态装载的目的。然而其类装载器委托模型在保证了安全性和强大功能的同时,也导致了相当的复杂性,有很多地方一旦我们不加注意的话就将导致错误。这里我希望通过一些小例子来展示动态装载的某些方面,深入地了解一下怎么进行动态装载,会遇到什么样的问题,并就问题的原因与解决方法进行讨论。
也许有人会说:我的程序不用什么动态装载,平时运行程序就是java –classpath … myPkg.my不就行了吗?不过我总是听前辈说,要想完全了解一个系统下的程序设计,就必须要深入研究这个平台的特性,做到心中有数,做起程序才不会处处制肘。类的动态装载是Java平台最显著的特征之一,许多著名的项目—Tomcat,Eclipse都使用自定义类装载器来装载运行时所需的类,如果连什么是动态装载和怎么动态装载都没弄明白,还能去玩自定义类装载器吗?所以还是不要浮躁,让我们从基础开始做起吧。
不过,我要说明的是你在看我这篇文章之前最好先熟悉一下Java的类装载器体系结构以及其委托模型,我在这里将不加赘述,推荐先看《Java深度历险》或《深入Java虚拟机》的第八章。
2 问题研究
我要讨论的问题是:让一个在my/目录下名为my.Main的类在运行期读取util/中的util.Tool类。
l 实验一
我的实验目录结构如下


其中my.Main的代码如下:

util.Tool的代码如下:

在my.Main的代码中16行可以看到,我定义了一个URLClassLoader,想要读取在同一根目录下的/util/Tool.class,接下来我们运行如下命令,来查看运行结果:

如上可见,util.Tool被顺利装载了,可是装载它的却是AppClassLoader,而不是我们想要的URLClassLoader!这是为什么呢?
原因很简单,因为我们将util/Tool.class放到了JVM系统变量”user.dir”所指定的目录下,而该JVM系统变量的默认值为程序所在的根目录,相当于把classpath 设置成了
”.”,也就是相当于执行了命令:
java –classpath . my.Main
于是,正因为util.Tool在classpath下,于是JVM就在在载入my.Main的同时也用AppClassLoader将util.Tool载入到JVM了。我们可以通过如下命令看得更清楚一些:
java –verbose my.Main

看到了吗?由于AppClassLoader预先载入了util.Tool类,而且在URLClassLoader的loadClass()方法中有如下语句:Class cls = getLoadedClass(String className);如果cls!=null的话,loadClass()就会返回已经载入过的(即由AppClassLoader所载入的)util.Tool类,所以我们才会看到以上结果。
l 实验二
为了解决以上问题,我决定按如下方式组织我的目录结构

然后对Main.class进行一些修改:

嗯,这下我们不会为util.Tool被提前载入而烦恼了,因为载入my.Main的AppClassLoader找不到换了位置的util.Tool类了。这回可能有人会问:subdir不是还在”user.dir”所指向的目录下吗?怎么只是多建了一层目录它就找不到了呢?原因是AppClassLoader是URLClassLoader的子类,它在载入class的时候也是根据该类的URL来进行定位的。那我们的例子来说,在“实验一”的最后一个图中我们可以看到,util.Tool被载入的位置为:
file1:c:/Test1,JVM得到这个URL之后,将util.Tool中的”.”变为”/”,在所得字符串结尾处加上”.class”,最后将该字符串加到
file1:c:/Test1后面,就得到了最终util.Tool的URL:
file1:c:/Test1/util/Tool.class。而当加了一层subdir目录之后,AppClassLoader就找不到
file1:c:/Test1/util/Tool.class了,而只能由程序中定义的URLClassLoader来载入。
这时我们运行程序,所得结果如下:

可以看到,util.Tool确实已经我定义的URLClassLoader被载入并初始化了,可是为什么最后却有一个奇怪的异常呢?NoClassDefFoundError?util.Tool类不是都载入了吗?怎么还找不到它的定义呢?
这个问题需要从JVM的装载机制说起。对于每个JVM实例,除了内置的三个内置的类装载器(BootStrapClassLoader,ExtClassLoader,AppClassLoader)之外,还可能有一些用户自定义的类装载器,而这些类装载器可能在一个程序运行周期内载入同一个类,即AppClassLoader和我自定义的类装载器MyClassLoader可能同时需要载入util.Tool类。为了避免冲突,JVM为每个类装载器都建立一个命名空间,并以此作为其访问边界。例如,如果my.Main想要引用util.Tool类,则它们必须是由同一装载器载入的,也就是说,由于my.Main是由AppClassLoader载入的,那么util.Tool也必须由AppClassLoader载入才行,否则就会出现上述错误。
问题解决的方法很简单,只需要让my.Main和util.Tool由同一类装载器载入就行了。
l 实验三
为了让my.Main和util.Tool由同一类装载器载入,我定义了一个启动类—boot.BootStrap类:


在这里,我用自己的类装载器ucl来装载my.Main类,并用reflection API来调用其main()方法,同时也对my.Main稍作修改

然后执行一下看看结果:
