这个讲起来挺复杂的,下面的资料你看下,应该就会明白的
引用自:http://hi.baidu.com/toponer/blog/item/120faa4402c93b4f510ffe15.html
每个java开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉 及到 了java技术体系中的类加载。Java的类加载机制是java技术体系中比较核心的部分,虽然和大部分开发人 员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对 理解java虚拟机的连接模型和java语言的动态性都有很大帮助。
由于关于java类加载的内容较多,所以打算分三篇文章简述一下:第一篇:类加载,类加载器,双亲委派,自定义类加载器第二篇:插件环境 Bundle类加载器第三篇:线程上下文类加载器作者:朱兴 查看本篇第一部分内容 3 程序动态扩展方式java Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义 的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。 通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连 接它们并进行有选择的解析。 运行时动态扩展java应用程序有如下两个 途径: 3.1 调用 java.lang.Class.forName(…) 这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发那个类加载器开始 加载任务。这里需要说明的是多参数版本的forName(…)方法: public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException 这里的initialize参数是很重要的,可以觉得被加载同时是否完成初始化的工作( 说明: 单参数版本的forName方法默认是不完成初始化的).有些场景下,需要将 initialize设 置为true来强制加载同时完成初始化,例如典型的就是利用 DriverManager进行JDBC驱动程序类注册的问题,因为每一个JDBC驱动程序类的静态初始化方法都用 DriverManager注册驱动程序,这样才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加 载. 3.2 用户自定义类加载器 通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类 加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否 被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍, 这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看): 1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回; 否则转入步骤2 2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真个虚拟机中各种类加载器最 终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤 3 3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调 用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异 常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种 类不止一种)。 (说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下) 4 常见问题分析: 4.1 由不同的类加载器 加载的指定类型还是相同的类型吗? 在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配 类名包括包名和类名。但在JVM中一个类用其全名和一个 加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名 空间.我 们可以用两个自定义类加载器去加载某自定义类型(注意,不要将自定义类型的字节码 放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两 个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写 两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后 再对比测试一下测试结果。 4.2 在代码中直接调用 Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为? Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对 应的jdk的代码:
//java.lang.Class.java public static Class<?> forName(String className)throws ClassNotFoundException { return forName0(className, true, ClassLoader.getCallerClassLoader()); } //java.lang.ClassLoader.java // Returns the invoker's class loader, or null if none. static ClassLoader getCallerClassLoader() { // 获取调用类(caller)的类型 Class caller = Reflection.getCallerClass(3); // This can be null if the VM is requesting it if (caller == null) { return null; } // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader return caller.getClassLoader0(); } //java.lang.Class.java //虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使 用此方法 native ClassLoader getClassLoader0();
4.3 在编写自定义类加 载器时,如果没有设定父加载器,那么父加载器是?前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我 们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自 java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:
//摘自java.lang.ClassLoader.java protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } this.parent = getSystemClassLoader(); initialized = true; }
我们再来看一下对应的getSystemClassLoader()方法的实现:
private static synchronized void initSystemClassLoader() { //... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); scl = l.getClassLoader(); //... }
我们可以写简单的测试代码来测试一下: System.out.println(sun.misc.Launcher.getLauncher ().getClassLoader()); 本机对应输出如下: AppClassLoader@197d257">sun.misc.Launcher$AppClassLoader@197d257 所以,我们现在可以相信当自定义类加载器没有指定父类加载器 的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论: 即时用户自定义类加载器不指定父类加载器,那么,同样可以加 载如下三个地方的类: 1. <Java_Runtime_Home>/lib下的类 2. < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类 3. 当前工程类 路径下或者由系统变量java.class.path指定位置中的类 4.4 在编写自定义类加 载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类 ,就肯定会加载失败吗? JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载 器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可 以得出如下结论: 即时用户自定义类加载器不指定父类加载器,那么,同样可以加载到 <Java_Runtime_Home>/lib下的类,但此时就不能 够加载<Java_Runtime_Home>/lib/ext目录下的类 了。 说明:问题 3和问题4 的推断结论是基于用户自定义的类加载器本身延续了 java.lang.ClassLoader.loadClass (…)默认委派逻辑,如果用户对这一默认委派逻 辑进行了改变,以上推断结论就不一定成立了,详见问题 5。 4.5 编写自定义类加载器时,一般有哪些注意点? 1. 一般尽量不要覆 写已有的loadClass(…)方法中的委 派逻辑一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能 正常工作。在JVM规范和JDK文档中 (1.2或者以后版本中),都没有建议用户覆写loadClass(…) 方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写 findClass(…)逻辑。举一个例子来验证该问题:
//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑) public class WrongClassLoader extends ClassLoader { public Class<?> loadClass(String name) throws ClassNotFoundException { return this.findClass(name); } protected Class<?> findClass(String name) throws ClassNotFoundException { //假设此处只是到工程以外的特定目录D:/library下去加载类 具体实现代码省略 } }
通过前面的分析我们已经知道,用户自定义类加 载器(WrongClassLoader)的默 认的类加载器是系统类加载 器,但是现在问题4种的结论就不成立了。大家可以简 单测试一下,现在 <Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工 程类路径上的类都加载不上 了。
//问题5测试代码一 public class WrongClassLoaderTest { public static void main(String[] args) { try { WrongClassLoader loader = new WrongClassLoader(); Class classLoaded = loader.loadClass("beans.Account"); System.out.println(classLoaded.getName()); System.out.println(classLoaded.getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
(说明:D:\classes\beans\Account.class物理存在的)输出结果: java.io.FileNotFoundException: D:\classes\java\lang\Object.class ( 系统找不到指定的路径。) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init> (FileInputStream.java:106) at WrongClassLoader.findClass (WrongClassLoader.java:40) at WrongClassLoader.loadClass (WrongClassLoader.java:29) at java.lang.ClassLoader.loadClassInternal (ClassLoader.java:319) at java.lang.ClassLoader.defineClass1 (Native Method) at java.lang.ClassLoader.defineClass (ClassLoader.java:620) at java.lang.ClassLoader.defineClass (ClassLoader.java:400) at WrongClassLoader.findClass (WrongClassLoader.java:43) at WrongClassLoader.loadClass (WrongClassLoader.java:29) at WrongClassLoaderTest.main (WrongClassLoaderTest.java:27) Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object at java.lang.ClassLoader.defineClass1 (Native Method) at java.lang.ClassLoader.defineClass (ClassLoader.java:620) at java.lang.ClassLoader.defineClass (ClassLoader.java:400) at WrongClassLoader.findClass (WrongClassLoader.java:43) at WrongClassLoader.loadClass (WrongClassLoader.java:29) at WrongClassLoaderTest.main (WrongClassLoaderTest.java:27) 这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于 覆写loadClass(…)引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。
//问题5测试二 //用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑) public class WrongClassLoader extends ClassLoader { protected Class<?> findClass(String name) throws ClassNotFoundException { //假设此处只是到工程以外的特定目录D:/library下去加载类 具体实现代码省略 } }
将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输 出结果如下: beans.Account WrongClassLoader@1c78e57 这说明,beans.Account加载成功,且是由自定义类加载器WrongClassLoader加载。 这其中的原因分析,我想这里就不必解释了,大家应该可以分析的出来了。 2. 2、正确设置父类加载器通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最 重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都 可以随便举出例子了。 3. 3、保证findClass( String )方法的逻辑正确性事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码 内容。
[此贴子已经被作者于2007-9-28 22:30:33编辑过]