yjwang
发贴: 0
积分: 0
|
于 2003-03-16 20:01
Java中的Classloader
作为一种编程语言, 我总觉得Java有那么一点奇异, 或者说混血: 它不象传统的编译型语言(比如C/C++)那么纯粹, 它不仅仅是一种"语言". 比如Java中有Classloader的概念, 而这通常是操作系统的一部分. 理解这一概念对于J2EE尤其重要. 下面的文章译自IBM的一篇文档, 很清楚地解释了这个重要的概念.
Classloader是如何工作的?
Java虚拟机中的每个Java类都是由某个classloader载入的, classloader本身也是Java类, 所以这一点就特别有趣. 那么, 这些classloader又是如何载入的呢? 这好像是一个悖论. 幸运的是, 事实并非如此. Java包含一个自举classloader, 它是用本地代码写的, 是JVM的一部分. 这个自举classloader的主要作用是载入Java核心类, 从而自举整个Java环境.
在一个企业Java应用中, 使用到的许多类都不是Java核心类. 比如, 程序员也许会引用其应用中的另外一个类, 或者Java扩展中的一个类. Java扩展是扩展Java核心平台功能的一些Java包. 为了隔离这两种不同的Java类, Java采用两种不同的classloader: application和extension classloader. 它们都是用Java写的. 这意味着这些类将被它们特定的classloader载入, 如下例所示. public class WhichClassLoader { WhichClassLoader( ) {} public static void main (String args[] ) throws Exception { //Retrieve the classpaths StringBuffer bootstrapClassPath=new StringBuffer(System.getProperties().getProperty("sun.boot.class.path")); StringBuffer extensionClassPath=new StringBuffer(System.getProperties().getProperty("java.ext.dirs")); StringBuffer systemClassPath=new StringBuffer(System.getProperties().getProperty("java.class.path")); System.out.println("\nBootstrap classpath= \n"+ bootstrapClassPath + "\n"); System.out.println("Extension classpath= "+ extensionClassPath + "\n"); System.out.println("System classpath= "+ systemClassPath + "\n" );
//Create new object instances java.lang.Object object = new java.lang.Object(); javax.naming.InitialContext initialContext = new javax.naming.InitialContext(); WhichClassLoader whichClassLoader = new WhichClassLoader(); System.out.println("\nJava Core file,java.lang.Object, was loaded by: " + object.getClass().getClassLoader()); System.out.println("\nExtension file, javax.naming.InitialContext, was loaded by: " + initialContext.getClass().getClassLoader()); System.out.println("\nUser file, WhichClassLoader, was loaded by: " + whichClassLoader1.getClass().getClassLoader() + "\n"); } }
这个例子相当简单, 它查询和显示classloader的路径, 然后创建三个新的对象示例, 类型各不相同: 一个Java核心类(java.lang.Object), 一个Java扩展类(javax.naming.InitialContext)和一个用户类(WhichClassLoader). 最后它打印出载入这些类的classloader. 运行这个例子的结果是: D:\Classpath_Project\src>java -classpath . WhichClassLoader Bootstrap classpath= D:\jdk1.2.2\jre\lib\rt.jar;D:\jdk1.2.2\jre\lib\i18n.jar;D:\jdk1.2.2\jre\classes Extension classpath= D:\jdk1.2.2\jre\lib\ext System classpath= Java Core file,java.lang.Object, was loaded by: null Extension file, javax.naming.InitialContext, was loaded by: sun.misc.Launcher$ExtClassLoader@f0272827 User file, WhichClassLoader, was loaded by: sun.misc.Launcher$AppClassLoader@f023282
这个结果中有几点值得一提. 比如, 你也许想知道为什么用户类是被sun.misc.Launcher$AppClassLoader@f023282所载入的, 而不是更通用的形式比如sun.misc.Launcher$AppClassLoader或者Application ClassLoader. 原因在于getClassLoader()方法的返回类型的是java.lang.ClassLoader. 当这个对象实例被送到一个输出流上时, 打印的是实例的名字, 这一名字在JVM每次启动时都会改变. 在我们这次特定的运行中, application classloader的实例名是sun.misc.Launcher$AppClassLoader@a1b1234. 对于extension classloader也是这样. 有趣的是, 自举classloader的实例名是空. 这是因为自举classloader不是用Java写的, 所以没有java.lang.ClassLoader的实例供返回.
另外一件引起你兴趣的事情也许是我们如何通过系统类查询路径, 比如System.getProperties().getProperty("sun.boot.class.path"). 在这个例子中, 你也许会想我们可以通过动态地改变路径, 从而强制用一个特定的classloader来载入一个特定的类. 我们可以这样修改来验证一下这个想法: import javax.naming.InitialContext; public class WhichClassLoader1 { //Constructor WhichClassLoader1( ) { //do nothing }
public static void main (String args[] ) throws Exception { //Retrieve the classpaths StringBuffer bootstrapClassPath=new StringBuffer(System.getProperties().getProperty("sun.boot.class.path")); StringBuffer extensionClassPath=new StringBuffer(System.getProperties().getProperty("java.ext.dirs")); StringBuffer systemClassPath=new StringBuffer(System.getProperties().getProperty("java.class.path"));
//modifying the bootstrapclasspath to include the jar file which contains the InitialContext Class // This should force the class to be loaded by the bootstrap classloader??????????? String fileSeparator = System.getProperty("file.separator"); String InitialContextJar = "D:"+fileSeparator+"jdk1.2.2"+fileSeparator + "jre"+fileSeparator+"lib"+fileSeparator+"ext"+fileSeparator+"iioprt.jar" bootstrapClassPath.append(System.getProperty("path.separator")).append(InitialContextJar); System.setProperty("sun.boot.class.path",bootstrapClassPath.toString()); System.out.println("\nBootstrap classpath=\n"+ bootstrapClassPath + "\n"); System.out.println("Extension classpath="+ extensionClassPath + "\n"); System.out.println("System classpath="+ systemClassPath + "\n" );
//Create new object instances Object object = new java.lang.Object() InitialContext initialContext = new InitialContext(); WhichClassLoader1 whichClassLoader1 = new WhichClassLoader1(); System.out.println("\nJava Core file,java.lang.Object, was loaded by: " + object.getClass().getClassLoader()); System.out.println("\nExtension file, Javax.naming.InitialContext, was loaded by: " + initialContext.getClass().getClassLoader()); System.out.println("\nUser file, WhichClassLoader1, was loaded by: " + whichClassLoader1.getClass().getClassLoader() + "\n"); } }
运行的结果很有趣: Bootstrap classpath=D:\jdk1.2.2\jre\lib\rt.jar;D:\jdk1.2.2\jre\lib\i18n.jar;D:\jdk1.2.2\jre\classes;D:\jdk1.2.2\jre\lib\ext\iioprt.jar Extension classpath=D:\jdk1.2.2\jre\lib\ext System classpath=. Java Core file,java.lang.Object, was loaded by: null Extension file, Javax.naming.InitialContext, was loaded by: sun.misc.Launcher$ExtClassLoader@f01b290a User file, WhichClassLoader1, was loaded by: sun.misc.Launcher$AppClassLoader@f01f290a
和我们期待的不同. 事实是改变这些系统级的属性完全没有效果. 这说明了这些classloader很重要的一个特性: 一旦JVM启动后, 它们的路径是不可更改的, 或者说是静态的.
让我们回到这个例子原来的目的. 到目前为止, 看上去Java的classloader正如我们被告知的那样工作: 自举classloader载入Java核心类, extension classloader载入Java扩展包, 而application classloader载入用户类. 然而, classloader在本质上又是分层的, 遵循"委托给父类"的模式. 这意味着除了自举classloader, 每一个classloader都有一个父classloader, 当一个classloader试图载入一个类时, 它首先将这一责任委托给它的父classloader. 这个模式是这样工作的: 一个classloader将尝试载入一个类, 当且仅当这个类在这个classloader所属的层次结构中还未被载入, 并且这个classloader的父classloader找不到这个类.
自顶向下地遍历classloader层次, 我们看到首先是自举classloader, 然后是extension classloader和application classloader. 如果你将classloader层次看作稀疏树结构, 那么自举classloader是根节点, application classloader是叶节点.
这种层次结构可以写程序演示如下: public class ShowHierarchy{ public static void main (String args[] ) throws Exception { System.out.println("The System ClassLoader is: " + ClassLoader.getSystemClassLoader().getClass()); System.out.println("The System ClassLoader's Parent is: " + ClassLoader.getSystemClassLoader().getParent().getClass()); System.out.println("The System ClassLoader's Parent's Parent is: " +ClassLoader.getSystemClassLoader().getParent().getParent()); } } 结果是: D:\WebSphere\Documents\Classpaths\src>java .classpath . ShowHierarchy The System ClassLoader is: class sun.misc.Launcher$AppClassLoader The System ClassLoader's Parent is: class sun.misc.Launcher$ExtClassLoader
为了演示"委托给父类"的关系, 让我们在每个classloader的路径上放一份包含InitialContext.class的JAR文件, 然后执行第一个例子. 记住一旦JVM运行以后, 它的classloader路径是不可变的, 所以我们需要通过Java开关-classpath和-bootclasspath在运行之前改变它们. D:\Classpath_Project\src\java -classpath D:\jdk1.2.2\jre\lib\ext\jndi.jar;. -XbootclasspathD:\jdk1.2.2\jre\lib\rt.jar;D:\jdk1.2.2\jre\lib\i18n.jar;D:\jdk1.2.2\jre\classes;D:\jdk1.2.2\jre\lib\ext\jndi.jar WhichClassLoader Bootstrap classpath= D:\jdk1.2.2\jre\lib\rt.jar;D:\jdk1.2.2\jre\lib\i18n.jar;D:\jdk1.2.2\jre\classes;D:\jdk1.2.2\jre\lib\ext\jndi.jar Extension classpath= D:\jdk1.2.2\jre\lib\ext System classpath= D:\jdk1.2.2\jre\lib\ext\jndi.jar;. Java Core file,java.lang.Object, was loaded by: null Extension file, javax.naming.InitialContext, was loaded by: null User file, WhichClassLoader, was loaded by: sun.misc.Launcher$AppClassLoader@f01a2987
正如你所见, InitialContext类如我们预见的那样是由自举classloader载入的, 你能理解这是为什么吗? 让我们详细描述一下发生的事吧: 1. WhichClassLoader类创建一个InitialContext类的新实例: javax.naming.InitialContext initialContext = new javax.naming.InitialContext(); 2. Application classloader查看InitialContext类是否已经在它所属的classloader层次结构中被载入, 也就是说被自举classloader, 被extension classloader或者被它自己所载入. 3. 结果是否定的. 4. 遵循"委托给父类"的模式, application classloader将载入的任务委托给extension classloader. 5. 再一次遵循"委托给父类"的模式, extension classloader将载入的任务委托给自举classloader. 6. 自举classloader没有父classloader, 尝试载入类. 7. 自举classloader成功地载入了InitialContext类. 8. InitialContext的新实例被创建并返回给WhichClassloader.
这一场景需要进一步的解释. 看上去似乎很明显的是, application classloader是第一个收到创建InitialContext新实例的classloader, 因为它位于classloader层次结构的最底层. 但并不总是这样. 随着Java 2的出现, 类将通过调用者的classloader被载入, 这可能是, 但也可能不是位于层次结构最底层的classloader. 在我们的例子中, 调用者是WhichClassLoader, 我们知道它是由application classloader载入的.
Java 2中的classloader遵循"委托给父类, 并组织成层次结构"的模式, 这允许你做一些有趣的事情. 但当类不是由classloader层次结构的叶节点载入时, 也可能引起问题. 比如, 让我们修改一下WhichClassLoader, 重命名为WhichClassLoader2, 并创建两个新类WhichClassLoader3和WhichClassLoader4.
文件WhichClassLoader2.java: public class WhichClassLoader2 { //Constructor WhichClassLoader2() { //do nothing } public static void main (String args[] ) throws Exception { //Retrieve the classpaths StringBuffer bootstrapClassPath=new StringBuffer(System.getProperties().getProperty("sun.boot.class.path")); StringBuffer extensionClassPath=new StringBuffer(System.getProperties().getProperty("java.ext.dirs")); StringBuffer systemClassPath=new StringBuffer(System.getProperties().getProperty("java.class.path"));
System.out.println("\nBootstrap classpath="+ bootstrapClassPath + "\n"); System.out.println("Extension classpath="+ extensionClassPath + "\n"); System.out.println("System classpath="+ systemClassPath + "\n" ); //Create new object instances java.lang.Object object = new java.lang.Object(); javax.naming.InitialContext initialContext = new javax.naming.InitialContext(); WhichClassLoader2 whichClassLoader2 = new WhichClassLoader2(); WhichClassLoader3 whichClassLoader3 = new WhichClassLoader3(); System.out.println("\nJava Core file,java.lang.Object, was loaded by: " + object.getClass().getClassLoader()); System.out.println("\nExtension file, javax.naming.InitialContext, was loaded by: " + initialContext.getClass().getClassLoader()); System.out.println("\nUser file, WhichClassLoader2, was loaded by: " + whichClassLoader2.getClass().getClassLoader() + "\n"); System.out.println("\nUser file, WhichClassLoader3, was loaded by: " + whichClassLoader3.getClass().getClassLoader() + "\n"); whichClassLoader3.getTheClass(); } }
文件WhichClassloader3.java: public class WhichClassLoader3 { public WhichClassLoader3 () { } public void getTheClass() { WhichClassLoader4 wcl4 = new WhichClassLoader4 (); System.out.println("WhichClassLoader4 was loaded by " + wcl4.getClass().getClassLoader()); } }
文件WhichClassloader4.java: public class WhichClassLoader4 { WhichClassLoader4 () { } }
我们将WhichClassLoader2和WhichClassLoader4放在当前目录下, 这样它们仅仅存在于application classloader的路径之中. 接下来我们将WhichClassLoader3放在extension classloader的路径中. D:\Classpath_Project\src>ls WhichClassLoader2.class WhichClassLoader2.java WhichClassLoader1.java WhichClassLoader4.class WhichClassLoader4.java D:\Classpath_Project\src>ls D:\jdk1.2.2\jre\lib\ext\ WhichClassLoader3.jar cosnaming.jar iiimp.jar iioprt.jar jndi.jar providerutil.jar rmiorb.jar rmiregistry.jar
我们来看一下运行WhichClassLoader2会发生什么. D:\Classpath_Project\src>java -classpath . WhichClassLoader2 Bootstrap classpath=D:\jdk1.2.2\jre\lib\rt.jar;D:\jdk1.2.2\jre\lib\i18n.jar;D:\jdk1.2.2\jre\classes Extension classpath=D:\jdk1.2.2\jre\lib\ext System classpath=. Java Core file,java.lang.Object, was loaded by: null Extension file, javax.naming.InitialContext, was loaded by: sun.misc.Launcher$ExtClassLoader@f01b3492 User file, WhichClassLoader2, was loaded by: sun.misc.Launcher$AppClassLoader@f01f3492 User file, WhichClassLoader3, was loaded by: sun.misc.Launcher$ExtClassLoader@f01b3492 java.lang.NoClassDefFoundError: WhichClassLoader4 at WhichClassLoader3.getTheClass(WhichClassLoader3.java:8) at WhichClassLoaderTest.main(WhichClassLoaderTest.java:38) Exception in thread "main"
你也许会问既然WhichClassLoader4明明在application classloader的路径中, 载入WhichClassLoader4为什么会失败. 这种失败不是因为classloader找不到类, 而是因为WhichClassLoader3的classloader找不到类. 为了更好地理解发生的事情, 让我们从载入WhichClassLoader3的classloader(也就是extension classloader)的角度来看一下.
首先, 要求创建一个WhichClassLoader4的实例, 于是遵循Java 2的"委托给父类"模式, extension classloader先查看这个类是否已经被它所属的classloader层次结构所载入, 这种尝试将失败. 于是它请求它的父classloader, 就是自举classloader, 来载入这个类. 这一请求将返回一个NoClassDefFound异常. Extension classloader将捕获这个异常并求助于它的最后选择: 它试图自己载入这个类. 搜寻了自己的搜索路径之后, extension classloader找不到WhichClassLoader4类的定义, 所以它重新抛出NoClassDefFoundError异常. 此时不再有classloader来捕获这个异常, 于是在屏幕上打印异常, 程序退出.
这就引起一个问题, 既然有可能发生这样的问题, 为什么要自寻烦恼地采用多个classloader呢? 为什么不回到Java 2之前的框架, 在那样的框架之下, 只有一个系统classloader来载入所有东西? 与引发的问题相比, classloader层次结构和"委托给父类"模式带来的好处是否值得呢? 简而言之, 是值得的: 1. 保护. Java预定义了自举classloader和extension classloader, 缺省情况下, 只把application classloader的定义留给了用户. 因为"委托给父类"的模式, 用户定义的任何类都不可能覆盖Java扩展类和核心类. 2. 用户友好. 在同样的方式下, 因为自举classloader和extension classloader是预定义的, 你不再需要把classes.zip文件放在-classpath属性中. 3. 隔离. 也许这是新的classloader定义做提供的最重要的好处. 到目前为止, 这看上去不是那么有用, 但我们还没有谈到Java允许你定义自己的classloader. 使用继承, 可以形成自己的classloader层次树.
理解这个模型所提供的益处的最佳方式是和操作系统的名字空间作一个类比. 让我们假设我们正在开发一个叫MyApp的应用, 这个应用有三个版本: 一个是实际用的, 一个处于Beta测试中, 一个还在开发中. 我们想要在任何时候运行这些应用程序版本中的任何一个, 于是我们将它们放在分开的目录结构中: /usr/MyApp /usr/MyApp/v1 /usr/MyApp/v2 /usr/MyApp/v3
所有版本公用的文件放在MyApp目录下, 版本特定的文件分别放在v1, v2和v3目录下. 使用这种方法, 我们能很有效地使用硬盘空间, 同时又能管理版本的差异. 同样的想法对classloader也适用. 核心类是最通用的, 就放在层次结构的根节点上, 而那些特定"版本"的类放在叶节点上.
译者注: 比较了WebSphere, WebLogic和JBoss之后, 发现它们的classloader设计都不一样. 移植代码时, 这可能引发一些问题.
|