深入类别载入器 forName() loadClass()
转自:http://kevin_yang.itpub.net/post/172/31025
1.java执行
java.exe 是利用几个基本原则来寻找Java
Runtime Environment(JRE),然后把类别档(.class)直接转交给JRE 执行之后,java.exe 就功成身退。类别加载器也是构成JRE 的其中一个重要成员,所以最后类别加载器就会自动从所在之JRE 目录底下的librt.jar 载入基础类别函式库。所以在上图里,一定是因为java.exe 定位到c:j2sdk1.4.0jre,所以才会有此输出结果。
2.预先加载与依需求加载
像基础类别函式库这样的加载方法我们叫做预先加载(pre-loading),这是因为基础类别函式库里头的类别大多是Java 程序执行时所必备的类别,所以为了不要老是做浪费时间的I/O 动作(读取档案系统,然后将类别文件加载内存之中),预先加载这些类别会让Java 应用程序在执行时速度稍微快一些。相对来说,我们自己所撰写的类别之加载方式,叫做依需求加载(load-on-demand),也就是Java 程序真正用到该类别的时候,才真的把类别文件从档案系统之中加载内存
3.Java 提供两种方法来达成动态性
一种是隐式的(implicit),另一种是显式的(explicit)。
隐式的(implicit)方法我们已经谈过了,也就是当程序设计师用到new 这个Java 关键词时,会让类别加载器依需求加载您所需要的类别,这种方式使用了隐式的(implicit)方法。显式的方法,又分成两种方式,一种是藉由java.lang.Class 里的forName()方法,另一种则是藉由java.lang.ClassLoader 里的loadClass()方法。
4.forName方法
public static Class forName(String name, boolean initialize,
ClassLoader loader)
这两个方法,最后都是连接到原生方法forName0(),其宣告如下:
private static native Class forName0(String name, boolean initialize, ClassLoader loader)
throws ClassNotFoundException;只有一个参数的forName()方法,最后叫用的是:
forName0(className, true, ClassLoader.getCallerClassLoader());而具有三个参数的forName()方法,最后叫用的是:forName0(name, initialize, loader);initialized的用法:true,表示载入实例的同时也载入静态初始化区块;false,那么就只会命令类别加载器加载该类别,但不会叫用其静态初始化区块,只有等到整个程序第一次实体化某个类别时,静态初始化区块才会被调用。
档案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Office off = new Office() ;
System.out.println("类别准备载入") ;
Class c =
Class.forName(args[0],false,off.getClass().getClassLoader()) ;
System.out.println("类别准备实体化") ;
Object o = c.newInstance() ;
Object o2 = c.newInstance() ;
}
}
则输出变成:
5.用显式的方法来达成动态性:直接使用类别加载器
这种情形与使用Class 类别的forName()方法时,第二个参数传入false
几乎是相同的结果。
档案:Office.java
public class Office
{
public static void main(String args[]) throws Exception
{
Office off = new Office() ;
System.out.println("类别准备载入") ;
ClassLoader loader = off.getClass().getClassLoader() ;
Class c = loader.loadClass(args[0]) ;
System.out.println("类别准备实体化") ;
Object o = c.newInstance() ;
Object o2 = c.newInstance() ;
}
}
执行
java Office Word
6.自己建立类别加载器来加载类别
档案:Office.java
import java.net.* ;
public class Office
{
public static void main(String args[]) throws Exception
{
URL u = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ;
Class c = ucl.loadClass(args[0]) ;
Assembly asm = (Assembly) c.newInstance() ;
asm.start() ;
}
}
7.类别被哪个类别载入器载入
我们将上述的程序代码稍作修改,修改后的程序代码如下:
档案:Office.java
import java.net.* ;
public class Office
{
public static void main(String args[]) throws Exception
{
URL u = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl = new URLClassLoader(new URL[]{ u }) ;
Class c = ucl.loadClass(args[0]) ;
Assembly asm = (Assembly) c.newInstance() ;
asm.start() ;
URL u1 = new URL("file:/d:/my/lib/") ;
URLClassLoader ucl1 = new URLClassLoader(new URL[]{ u1 }) ;
Class c1 = ucl1.loadClass(args[0]) ;
Assembly asm1 = (Assembly) c1.newInstance() ;
asm1.start() ;
System.out.println(Office.class.getClassLoader()) ;
System.out.println(u.getClass().getClassLoader()) ;
System.out.println(ucl.getClass().getClassLoader()) ;
System.out.println(c.getClassLoader()) ;
System.out.println(asm.getClass().getClassLoader()) ;
System.out.println(u1.getClass().getClassLoader()) ;
System.out.println(ucl1.getClass().getClassLoader()) ;
System.out.println(c1.getClassLoader()) ;
System.out.println(asm1.getClass().getClassLoader()) ;
}
}
执行后输出结果如下图:
从输出中我们可以得知,Office.class 由AppClassLoader(又称做System Loader,系统加载器)所加载,URL.class 与URLClassLoader.class 由Bootstrap Loader 所加载(注意:输出null 并非代表不是由类别载入器所载入。在Java 之中,所有的类别都必须由类别加载器加载才行,只不过Bootstrap Loader 并非由Java 所撰写而成,而是由C++实作而成,因此以Java 的观点来看,逻辑上并没有Bootstrap Loader 的类别实体)。而Word.class 分别由两个不同的URLClassLoader 实体加载。至于Assembly.class,本身应该是由AppClassLoader 加载,但是由于多型(Polymorphism)的关系,所指向的类别实体(Word.class)由特定的加载器所加载,导致打印在屏幕上的内容是其所参考的类别实体之类别加载器。Interface 这种型态本身无法直接使用new 来产生实体,所以在执行getClassLoader()的时候,叫用的一定是所参考的类别实体的getClassLoader(),要知道Interface 本身由哪个类别加载器加载,您必须使用底下程序代码:Assembly.class.getClassLoader()
8.一切都是由Bootstrap Loader 开始 : 类别载入器的阶层体系
当我们在命令列输入java xxx.class 的时候,java.exe 根据我们之前所提过的逻辑找到了
JRE(Java Runtime Environment),接着找到位在JRE 之中的jvm.dll(真正的Java 虚拟机器),最后加载这个动态联结函式库,启动Java 虚拟机器。这个动作的详细介绍请回头参阅第一章。虚拟机器一启动,会先做一些初始化的动作,比方说抓取系统参数等。一旦初始化动作完成之后,就会产生第一个类别载入器,即所谓的Bootstrap Loader,Bootstrap Loader 是由C++所撰写而成(所以前面我们说,以Java 的观点来看,逻辑上并不存在Bootstrap Loader 的类别实体,所以在Java 程序代码里试图印出其内容的时候,我们会看到的输出为null),这个Bootstrap Loader 所做的初始工作中,除了也做一些基本的初始化动作之外,最重要的就是加载定义在sun.misc 命名空间底下的Launcher.java 之中的ExtClassLoader(因为是inner class,所以编译之后会变成
Launcher$ExtClassLoader.class),并设定其Parent 为null,代表其父加载器为Bootstrap
Loader。然后Bootstrap Loader 再要求加载定义于sun.misc 命名空间底下的Launcher.java 之中的AppClassLoader(因为是inner class,所以编译之后会变成Launcher$AppClassLoader.class),并设定其Parent 为之前产生的ExtClassLoader 实体。这里要请大家注意的是,Launcher$ExtClassLoader.class 与Launcher$AppClassLoader.class 都是由Bootstrap Loader 所加载,所以Parent 和由哪个类别加载器加载没有关系。我们可以用下图来表示:
这个由Bootstrap Loader ExtClassLoader AppClassLoader,就是我们所谓「类别载入
器的阶层体系」。
在此要请大家注意的是,AppClassLoader 和ExtClassLoader 都是URLClassLoader 的子类别。由于它们都是URLClassLoader 的子类别,所以它们也应该有URL 作为搜寻类别档的参考,由原始码中我们可以得知,AppClassLoader 所参考的URL 是从系统参数java.class.path 取出的字符串所决定,而java.class.path 则是由我们在执行java.exe 时,利用 –cp 或-classpath 或CLASSPATH 环境变量所决定。我们可以用底下程序代码测试之:
档案:test.java
public class test
{
public static void main(String args[])
{
String s = System.getProperty("java.class.path");
System.out.println(s) ;
}
}
输出结果如下:
从这个输出结果,我们可以看出,在预设情况下,AppClassLoader 的搜寻路径为”.”(目前所在目录),如果使用-classpath 选项(与-cp 等效),就可以改变AppClassLoader 的搜寻路径,如果没有指定-classpath 选项,就会搜寻环境变量CLASSPATH。如果同时有CLASSPATH 的环境设定与-classpath 选项,则以-classpath 选项的内容为主,CLASSPATH 的环境设定与-classpath 选项两者的内容不会有加成的效果。
至于ExtClassLoader 也有相同的情形,不过其搜寻路径是参考系统参数 java.ext.dirs。我们可以用底下程序代码测试:
档案:test.java
public class test
{
public static void main(String args[])
{
String s = System.getProperty("java.ext.dirs");
System.out.println(s) ;
}
}
输出结果如下:
输出结果告诉我们,系统参数java.ext.dirs 的内容,会指向java.exe 所选择的JRE 所在位置下的
libext 子目录。系统参数java.ext.dirs 的内容可以在一开始下命列的时候来更改,如下:
最后一个类别加载器是Bootstrap Loader , 我们可以经由查询由系统参数
sun.boot.class.path 得知Bootstrap Loader 用来搜寻类别的路径。请使用底下的程序代码测试之:
档案:test.java
public class test
{
public static void main(String args[])
{
String s = System.getProperty("sun.boot.class.path");
System.out.println(s) ;
}
}
输出结果如下:
系统参数sun.boot.class.path 的内容可以在一开始下命列的时候来更改,如下:
从这三个类别加载器的搜寻路径所参考的系统参数的名字中,其实还透漏了一个讯息。请回头看到java.class.path 与sun.boot.class.path,也就是说,AppClassLoader 与Bootstrap Loader会搜寻它们所指定的位置(或JAR 文件),如果找不到就找不到了,AppClassLoader 与Bootstrap Loader不会递归式地搜寻这些位置下的其它路径或其它没有被指定的JAR 檔。反观ExtClassLoader,所参考的系统参数是java.ext.dirs,意思是说,他会搜寻底下的所有JAR 文件以及classes 目录,作为其搜寻路径(所以您会发现上面我们在测试的时候,如果加入 -Dsun.boot.class.path=c:winnt选项时,程序的起始速度会慢了些,这是因为c:winnt 目录下的档案很多,必须花额外的时间来列举JAR 檔)。
在命令列下参数时,使用 –classpath / -cp / 环境变量CLASSPATH 来更改AppClassLoader
的搜寻路径,或者用 –Djava.ext.dirs 来改变ExtClassLoader 的搜寻目录,两者都是有意义的。可是用–Dsun.boot.class.path 来改变Bootstrap Loader 的搜寻路径是无效。这是因为AppClassLoader 与ExtClassLoader 都是各自参考这两个系统参数的内容而建立,当您在命令列下变更这两个系统参数之后, AppClassLoader 与ExtClassLoader 在建立实体的时候会参考这两个系统参数,因而改变了它们搜寻类别文件的路径;而系统参数sun.boot.class.path 则是预设与Bootstrap Loader 的搜寻路径相同,就算您更改该系统参与,与Bootstrap Loader 完全无关。如果您手边有原始码, 以JDK 1.4.x 为例, 请参考< 原始码根目录>hotspotsrcsharevmruntimeos.cpp 这个档案里头的os::set_boot_path 方法,您将看到程序代码片段:
static const char classpathFormat[] =
"%/lib/rt.jar:"
"%/lib/i18n.jar:"
"%/lib/sunrsasign.jar:"
"%/lib/jsse.jar:"
"%/lib/jce.jar:"
"%/lib/charsets.jar:"
"%/classes";
JDK 1.4.x 比JDK 1.3.x 新增了一些核心类别函式库(例如jsse.jar 与jce.jar),所以如果您测试时用的是1.4.x 版的JDK,sun.boot.class.path 内容应该如下:
9. 委派模型
如果您对整个类别加载的方式仍有所疑问,请容笔者重新解释一下之前程序
档案:Office.java
import java.net.* ;
public class Office
{
public static void main(String args[]) throws Exception
{