一. Class Loader(类装载器)
1. 类装载器结构
类装载器(Class Loader)是Java虚拟机的组成部分之一,如图1所示.Java虚拟机有两种类装载器:启动类装载器和用户自定义类装载器.
URL:http://blog.blogchina.com/upload/2004-11-24/20041124104254943028.jpg
图1:Java虚拟机的内部体系结构.
启动类装载器 每个Java虚拟机实现都必须有一个启动类装载器.在Sun的JDK 1.2以后的版本中,启动类装载器只负责在系统类(核心Java API的class文件)的安装路径中查找要装入的类.
用户自定义类装载器 它是普通的Java对象,它的类必须继承自java.lang.ClassLoader类.在Sun的JDK 1.2以后的版本中,用户自定义类装载器负责核心Java API以外的其它class文件的装载.例如,用于安装或下载标准扩展的class文件,在类路径中发现的类库的class文件,用于应用程序运行的class文件,等等.
命名空间 Java虚拟机为每一个类装载器维护一个唯一标识的命名空间.一个Java程序可以多次装载具有同一个全限定名(指类所属的包名加类名,如java.lang.Object就是类Object的全限定名)的多个类(class). Java虚拟机要确定这"多个类"的唯一性,因此,当多个类装载器都装载了同名的类时,为了唯一地标识这个类,还要在类名前加上装载该类的类装载器的标识(指出了类所位于的命名空间).例如:ExtClassLoader 装载了sun.text.resources.DateFormatZoneData_zh_CN类,AppClassLoader装载了sun.text.resources.DateFormatZoneData_zh_HK类, Java虚拟机就认为这两个类位于不同的包中,彼此之间不能访问私有成员.如果AppClassLoader也装载了sun.text.resources.DateFormatZoneData_zh_CN类, 虽然"类名"相同,Java虚拟机也认为它们是不同的类,因为它们处在不同的命名空间中.
2. 委托(Delegation)模型
当Java虚拟机开始运行时,在应用程序开始启动以前,它至少创建一个用户自定义装载器,也可能创建多个.所有这些装载器被连接在一个Parent-Child的委托链中,在这个链的顶端是启动类装载器,末端是被称为"系统类装载器"的类装载器.
例如,假设你写了一个应用程序,在虚拟机上运行它.虚拟机在启动时实例化了两个用户自定义类装载器:一个"扩展类装载器",一个"类路径类装载器".这些类装载器和启动类装载器一起联入一个Parent-Child委托链中,如图2所示.
URL:http://blog.blogchina.com/upload/2004-11-24/2004112410430176267.jpg
图2arent-Child类装载器委托链
类路径类装载器的Parent是扩展类装载器, 扩展类装载器的Parent是启动类装载器.在图2中,类路径类装载器就被实例为系统类装载器.假设你的程序实例化它的网络类装载器,它就指明了系统类装载器作为它的Parent.
下面的例程说明了类装载器的父子关系.
例程1:
package test;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderTest {
private static int count = -1;
public static void testClassLoader(Object obj) {
if (count < 0 && obj == null) {
System.out.println("Input object is NULL";
return;
}
ClassLoader cl = null;
if (obj != null && !(obj instanceof ClassLoader)) {
cl = obj.getClass().getClassLoader();
} else if (obj != null) {
cl = (ClassLoader) obj;
}
count++;
String parent = "";
for (int i = 0; i < count; i++) {
parent += "arent ";
}
if (cl != null) {
System.out.println(
parent + "ClassLoader name = " + cl.getClass().getName());
testClassLoader(cl.getParent());
} else {
System.out.println(
parent + "ClassLoader name = BootstrapClassLoader";
count = -1;
}
}
public static void main(String[] args) {
URL[] urls = new URL[1];
URLClassLoader urlLoader = new URLClassLoader(urls);
ClassLoaderTest.testClassLoader(urlLoader);
}
}
以上例程的输出为:
ClassLoader name = java.net.URLClassLoader
Parent ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent Parent ClassLoader name = BootstrapClassLoader
类装载器请求过程
以上例程1为例.将main方法改为:
ClassLoaderTest tc = new ClassLoaderTest();
ClassLoaderTest.testClassLoader(tc);
输出为:
ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent ClassLoader name = BootstrapClassLoader
程序运行过程中,类路径类装载器发出一个装载ClassLoaderTest类的请求, 类路径类装载器必须首先询问它的Parent---扩展类装载器---来查找并装载这个类,同样扩展类装载器首先询问启动类装载器.由于ClassLoaderTest不是Java API(JAVA_HOME/jre/lib)中的类,也不在已安装扩展路径(JAVA_HOME/jre/lib/ext)上,这两类装载器都将返回而不会提供一个名为ClassLoaderTest的已装载类给类路径类装载器.类路径类装载器只能以它自己的方式来装载ClassLoaderTest,它会从当前类路径上下载这个类.这样,ClassLoaderTest就可以在应用程序后面的执行中发挥作用.
在上例中, ClassLoaderTest类的testClassLoader方法被首次调用,该方法引用了Java API中的类java.lang.String.Java虚拟机会请求装载ClassLoaderTest类的类路径类装载器来装载java.lang.String.就像前面一样,类路径类装载器首先将请求传递给它的Parent类装载器,然后这个请求一路被委托到启动类装载器.但是,启动类装载器可以将java.lang.String类返回给类路径类装载器,因为它可以找到这个类,这样扩展类装载器就不必在已安装扩展路径中查找这个类,类路径类装载器也不必在类路径中查找这个类.扩展类装载器和类路径类装载器仅需要返回由启动类装载器返回的类java.lang.String.从这一刻开始,不管何时ClassLoaderTest类引用了名为java.lang.String的类,虚拟机就可以直接使用这个java.lang.String类了.
在上述过程中也可能会发生错误,在本文下面的例子中将会涉及.
3. 装载 连接及初始化
在一个Java类的生命周期中,装载,连接和初始化只是其开始阶段.只有开始阶段结束以后,类才可以被实例化并被使用.整个开始阶段必须按以下顺序进行:
1)装载 把二进制形式的Java class读入虚拟机中.
2)连接 把已经读入虚拟机的二进制形式的类数据合并到虚拟机的运行状态中去.连接阶段分为验证,准备和解析三个子步骤.
3)初始化 给类变量赋以适当的初始值.
Java虚拟机允许类装载器(启动或用户自定义类装载器)缓存Java class的二进制形式,在预知某个类将要被使用时就装载它.如果一个类装载器在预先装载时遇到问题,它应该在该类被"首次主动使用"时报告该问题(通过抛出一个java.lang.LinkageError的子类).也就是说,如果一个类装载器在预先装载时遇到缺失或错误的class文件,它必须等到程序首次被主动使用该类时才报告错误.如果这个类一直没有被程序主动使用,那么该类装载器将不会报告错误.
二. Tomcat5中的Class Loader
当Tomcat5启动的时候,它会首先创建一组class loader,如commonLoader, sharedLoader, catalinaLoader,webappLoader等.其委托模型如下图3所示:
Bootstrap
|
System
|
Common
/ /
Catalina Shared
/
Webapp1 ...
图3:Tomcat5类装载器委托模型
其中,
1) Bootstrap 该类装载器装载JAVA_HOME/jre/lib和JAVA_HOME/jre/lib/ext两目录上的JAR包.
2) System 该类装载器装载当前CLASSPATH上的JAR包.在Windows系统下, CLASSPATH环境变量会在CATALINA_HOME/bin/setclasspath.bat和CATALINA_HOME/bin/catalina.bat文件中被重新设置.
3) Common 该类装载器装载CATALINA_HOME/common/classes目录中的类, CATALINA_HOME/commons/endorsed和CATALINA_HOME/common/lib目录中的JAR包.
4) Catalina 该类装载器装载CATALINA_HOME/server/classes和CATALINA_HOME/server/lib目录中的类和JAR包.
5) Shared 该类装载器装载CATALINA_HOME/shared/classes和CATALINA_HOME/shared/lib目录中的类和JAR包.
6) WebappX 该类装载器装载WEB-INF/classes和WEB-INF/lib目录中的类和JAR包.
需要补充说明的是,WebappX类装载器独立于上文提到的Java2的委托模型.当WebappX类装载器装载一个类时,它会首先查找本身所辖目录(即WEB-INF/classes和WEB-INF/lib)下的类,而不会启动委托机制.当然对于Bootstrap和System类装载器中存在的类,是要进行委托的.另外,对于下面这些包中的类,如:
javax.*
org.xml.sax.*
org.w3c.dom.*
org.apache.xerces.*
org.apache.xalan.*
WebappX类装载器装载时也要启动委托机制.
例如,假设ojdbc14.jar处在setclasspath.bat中的CLASSPATH下,同时也处在WEB-INF/lib目录下.类装载器系统在请求装载oracle.jdbc.driver.OracleDriver类时,会得到从System类装载器返回的类,而不是WebappX类装载器.
再假设ojdbc14.jar处在CATALINA_HOME/common/lib和WEB-INF/lib目录下,而没有处在setclasspath.bat中的CLASSPATH下,那么类装载器系统就会得到从WebappX类装载器返回的类,而不是Common类装载器.
另外,如果WEB-INF/lib目录下存在包含有servlet API类的JAR包,该JAR包将会被WebappX类装载器忽略.例如,
考虑到应用程序的编译问题,你可能会把servlet-api.jar包Copy到应用程序中的WEB-INF/lib目录下.那么,在Tomcat5启动时,启动屏幕上就会出现如下输出:
2004/09/12 14:53:57 org.apache.catalina.loader.WebappClassLoader validateJarFile
情报: validateJarFile(E:/MyData/myProjects/MyBS_SQL/web/WEB-INF/lib/servlet-api.jar) - jar not loaded. See Servlet Spec 2.3, section 9.7.2. Offending class: javax/servlet/Servlet.class
上述信息并不影响你的程序运行.因为WebappX类装载器虽然会忽略掉WEB-INF/lib目录下servlet-api.jar包,但是,Common类装载器已经装载了servlet-api.jar包.如前所述,对于javax.*中的类, WebappX类装载器是要启动委托机制的,所以WebappX类装载器会得到Common类装载器返回的javax.*中的类.
如果不希望Tomcat5启动时输出上述信息,只需将servlet-api.jar包从应用程序中的WEB-INF/lib目录下移走就行了.
总结一下,Tomcat5类装载器系统在请求装载一个类时,它以下面列举的顺序进行:
· Bootstrap (JAVA_HOME/jre/lib和JAVA_HOME/jre/lib/ext)
· System (当前CLASSPATH上)
· /WEB-INF/classes 和/WEB-INF/lib/*.jar
· CATALINA_HOME/common/classes
· CATALINA_HOME/common/endorsed/*.jar
· CATALINA_HOME/common/lib/*.jar
· CATALINA_HOME/shared/classes 和CATALINA_HOME/shared/lib/*.jar
1. 类装载器结构
类装载器(Class Loader)是Java虚拟机的组成部分之一,如图1所示.Java虚拟机有两种类装载器:启动类装载器和用户自定义类装载器.
URL:http://blog.blogchina.com/upload/2004-11-24/20041124104254943028.jpg
图1:Java虚拟机的内部体系结构.
启动类装载器 每个Java虚拟机实现都必须有一个启动类装载器.在Sun的JDK 1.2以后的版本中,启动类装载器只负责在系统类(核心Java API的class文件)的安装路径中查找要装入的类.
用户自定义类装载器 它是普通的Java对象,它的类必须继承自java.lang.ClassLoader类.在Sun的JDK 1.2以后的版本中,用户自定义类装载器负责核心Java API以外的其它class文件的装载.例如,用于安装或下载标准扩展的class文件,在类路径中发现的类库的class文件,用于应用程序运行的class文件,等等.
命名空间 Java虚拟机为每一个类装载器维护一个唯一标识的命名空间.一个Java程序可以多次装载具有同一个全限定名(指类所属的包名加类名,如java.lang.Object就是类Object的全限定名)的多个类(class). Java虚拟机要确定这"多个类"的唯一性,因此,当多个类装载器都装载了同名的类时,为了唯一地标识这个类,还要在类名前加上装载该类的类装载器的标识(指出了类所位于的命名空间).例如:ExtClassLoader 装载了sun.text.resources.DateFormatZoneData_zh_CN类,AppClassLoader装载了sun.text.resources.DateFormatZoneData_zh_HK类, Java虚拟机就认为这两个类位于不同的包中,彼此之间不能访问私有成员.如果AppClassLoader也装载了sun.text.resources.DateFormatZoneData_zh_CN类, 虽然"类名"相同,Java虚拟机也认为它们是不同的类,因为它们处在不同的命名空间中.
2. 委托(Delegation)模型
当Java虚拟机开始运行时,在应用程序开始启动以前,它至少创建一个用户自定义装载器,也可能创建多个.所有这些装载器被连接在一个Parent-Child的委托链中,在这个链的顶端是启动类装载器,末端是被称为"系统类装载器"的类装载器.
例如,假设你写了一个应用程序,在虚拟机上运行它.虚拟机在启动时实例化了两个用户自定义类装载器:一个"扩展类装载器",一个"类路径类装载器".这些类装载器和启动类装载器一起联入一个Parent-Child委托链中,如图2所示.
URL:http://blog.blogchina.com/upload/2004-11-24/2004112410430176267.jpg
图2arent-Child类装载器委托链
类路径类装载器的Parent是扩展类装载器, 扩展类装载器的Parent是启动类装载器.在图2中,类路径类装载器就被实例为系统类装载器.假设你的程序实例化它的网络类装载器,它就指明了系统类装载器作为它的Parent.
下面的例程说明了类装载器的父子关系.
例程1:
package test;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderTest {
private static int count = -1;
public static void testClassLoader(Object obj) {
if (count < 0 && obj == null) {
System.out.println("Input object is NULL";
return;
}
ClassLoader cl = null;
if (obj != null && !(obj instanceof ClassLoader)) {
cl = obj.getClass().getClassLoader();
} else if (obj != null) {
cl = (ClassLoader) obj;
}
count++;
String parent = "";
for (int i = 0; i < count; i++) {
parent += "arent ";
}
if (cl != null) {
System.out.println(
parent + "ClassLoader name = " + cl.getClass().getName());
testClassLoader(cl.getParent());
} else {
System.out.println(
parent + "ClassLoader name = BootstrapClassLoader";
count = -1;
}
}
public static void main(String[] args) {
URL[] urls = new URL[1];
URLClassLoader urlLoader = new URLClassLoader(urls);
ClassLoaderTest.testClassLoader(urlLoader);
}
}
以上例程的输出为:
ClassLoader name = java.net.URLClassLoader
Parent ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent Parent ClassLoader name = BootstrapClassLoader
类装载器请求过程
以上例程1为例.将main方法改为:
ClassLoaderTest tc = new ClassLoaderTest();
ClassLoaderTest.testClassLoader(tc);
输出为:
ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent ClassLoader name = BootstrapClassLoader
程序运行过程中,类路径类装载器发出一个装载ClassLoaderTest类的请求, 类路径类装载器必须首先询问它的Parent---扩展类装载器---来查找并装载这个类,同样扩展类装载器首先询问启动类装载器.由于ClassLoaderTest不是Java API(JAVA_HOME/jre/lib)中的类,也不在已安装扩展路径(JAVA_HOME/jre/lib/ext)上,这两类装载器都将返回而不会提供一个名为ClassLoaderTest的已装载类给类路径类装载器.类路径类装载器只能以它自己的方式来装载ClassLoaderTest,它会从当前类路径上下载这个类.这样,ClassLoaderTest就可以在应用程序后面的执行中发挥作用.
在上例中, ClassLoaderTest类的testClassLoader方法被首次调用,该方法引用了Java API中的类java.lang.String.Java虚拟机会请求装载ClassLoaderTest类的类路径类装载器来装载java.lang.String.就像前面一样,类路径类装载器首先将请求传递给它的Parent类装载器,然后这个请求一路被委托到启动类装载器.但是,启动类装载器可以将java.lang.String类返回给类路径类装载器,因为它可以找到这个类,这样扩展类装载器就不必在已安装扩展路径中查找这个类,类路径类装载器也不必在类路径中查找这个类.扩展类装载器和类路径类装载器仅需要返回由启动类装载器返回的类java.lang.String.从这一刻开始,不管何时ClassLoaderTest类引用了名为java.lang.String的类,虚拟机就可以直接使用这个java.lang.String类了.
在上述过程中也可能会发生错误,在本文下面的例子中将会涉及.
3. 装载 连接及初始化
在一个Java类的生命周期中,装载,连接和初始化只是其开始阶段.只有开始阶段结束以后,类才可以被实例化并被使用.整个开始阶段必须按以下顺序进行:
1)装载 把二进制形式的Java class读入虚拟机中.
2)连接 把已经读入虚拟机的二进制形式的类数据合并到虚拟机的运行状态中去.连接阶段分为验证,准备和解析三个子步骤.
3)初始化 给类变量赋以适当的初始值.
Java虚拟机允许类装载器(启动或用户自定义类装载器)缓存Java class的二进制形式,在预知某个类将要被使用时就装载它.如果一个类装载器在预先装载时遇到问题,它应该在该类被"首次主动使用"时报告该问题(通过抛出一个java.lang.LinkageError的子类).也就是说,如果一个类装载器在预先装载时遇到缺失或错误的class文件,它必须等到程序首次被主动使用该类时才报告错误.如果这个类一直没有被程序主动使用,那么该类装载器将不会报告错误.
二. Tomcat5中的Class Loader
当Tomcat5启动的时候,它会首先创建一组class loader,如commonLoader, sharedLoader, catalinaLoader,webappLoader等.其委托模型如下图3所示:
Bootstrap
|
System
|
Common
/ /
Catalina Shared
/
Webapp1 ...
图3:Tomcat5类装载器委托模型
其中,
1) Bootstrap 该类装载器装载JAVA_HOME/jre/lib和JAVA_HOME/jre/lib/ext两目录上的JAR包.
2) System 该类装载器装载当前CLASSPATH上的JAR包.在Windows系统下, CLASSPATH环境变量会在CATALINA_HOME/bin/setclasspath.bat和CATALINA_HOME/bin/catalina.bat文件中被重新设置.
3) Common 该类装载器装载CATALINA_HOME/common/classes目录中的类, CATALINA_HOME/commons/endorsed和CATALINA_HOME/common/lib目录中的JAR包.
4) Catalina 该类装载器装载CATALINA_HOME/server/classes和CATALINA_HOME/server/lib目录中的类和JAR包.
5) Shared 该类装载器装载CATALINA_HOME/shared/classes和CATALINA_HOME/shared/lib目录中的类和JAR包.
6) WebappX 该类装载器装载WEB-INF/classes和WEB-INF/lib目录中的类和JAR包.
需要补充说明的是,WebappX类装载器独立于上文提到的Java2的委托模型.当WebappX类装载器装载一个类时,它会首先查找本身所辖目录(即WEB-INF/classes和WEB-INF/lib)下的类,而不会启动委托机制.当然对于Bootstrap和System类装载器中存在的类,是要进行委托的.另外,对于下面这些包中的类,如:
javax.*
org.xml.sax.*
org.w3c.dom.*
org.apache.xerces.*
org.apache.xalan.*
WebappX类装载器装载时也要启动委托机制.
例如,假设ojdbc14.jar处在setclasspath.bat中的CLASSPATH下,同时也处在WEB-INF/lib目录下.类装载器系统在请求装载oracle.jdbc.driver.OracleDriver类时,会得到从System类装载器返回的类,而不是WebappX类装载器.
再假设ojdbc14.jar处在CATALINA_HOME/common/lib和WEB-INF/lib目录下,而没有处在setclasspath.bat中的CLASSPATH下,那么类装载器系统就会得到从WebappX类装载器返回的类,而不是Common类装载器.
另外,如果WEB-INF/lib目录下存在包含有servlet API类的JAR包,该JAR包将会被WebappX类装载器忽略.例如,
考虑到应用程序的编译问题,你可能会把servlet-api.jar包Copy到应用程序中的WEB-INF/lib目录下.那么,在Tomcat5启动时,启动屏幕上就会出现如下输出:
2004/09/12 14:53:57 org.apache.catalina.loader.WebappClassLoader validateJarFile
情报: validateJarFile(E:/MyData/myProjects/MyBS_SQL/web/WEB-INF/lib/servlet-api.jar) - jar not loaded. See Servlet Spec 2.3, section 9.7.2. Offending class: javax/servlet/Servlet.class
上述信息并不影响你的程序运行.因为WebappX类装载器虽然会忽略掉WEB-INF/lib目录下servlet-api.jar包,但是,Common类装载器已经装载了servlet-api.jar包.如前所述,对于javax.*中的类, WebappX类装载器是要启动委托机制的,所以WebappX类装载器会得到Common类装载器返回的javax.*中的类.
如果不希望Tomcat5启动时输出上述信息,只需将servlet-api.jar包从应用程序中的WEB-INF/lib目录下移走就行了.
总结一下,Tomcat5类装载器系统在请求装载一个类时,它以下面列举的顺序进行:
· Bootstrap (JAVA_HOME/jre/lib和JAVA_HOME/jre/lib/ext)
· System (当前CLASSPATH上)
· /WEB-INF/classes 和/WEB-INF/lib/*.jar
· CATALINA_HOME/common/classes
· CATALINA_HOME/common/endorsed/*.jar
· CATALINA_HOME/common/lib/*.jar
· CATALINA_HOME/shared/classes 和CATALINA_HOME/shared/lib/*.jar