深入探讨Java类加载机制

一、前言

  毕业至今,已经三年光景,平时基本接触不到关于类加载器的技术(工作上),相信很多同行在开始工作后很长一段时间,对于类的加载机制都没有深入的了解过,之前偶然的机会接触了相关的知识,感觉挺有意思,所以这边摘抄和收集一下别人的帖子,整理记录一下,希望对处于java进阶的同行有所帮助~

二、类的加载过程

2.1 JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)。链接又分为三个步骤,如下图所示

 

 

 

1) 装载:查找并加载类的二进制数据

2) 链接:

验证:确保被加载类的正确性;

准备:为类的静态变量分配内存,并将其初始化为默认值;

解析:把类中的符号引用转换为直接引用;

3) 初始化:为类的静态变量赋予正确的初始值

      那为什么我要有验证这一步骤呢?首先如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途,就不妙了,因此这个class文件要先过验证这一关,不符合的话不会让它继续执行的,也是为了安全考虑吧。

      准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

 

2.2 类的初始化

类什么时候才被初始化:

      1)创建类的实例,也就是new一个对象

  2)访问某个类或接口的静态变量,或者对该静态变量赋值

  3)调用类的静态方法

  4)反射(Class.forName("com.lyj.load"))

  5)初始化一个类的子类(会首先初始化子类的父类)

  6)JVM启动时标明的启动类,即文件名和类名相同的那个类     

只有这6中情况才会导致类的类的初始化。类的初始化步骤:

        1)如果这个类还没有被加载和链接,那先进行加载和链接

        2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)

        3) 假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。

 

2.3 类的加载

      类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。看下面2图

      

 

 

    

 

      类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

  加载类的方式有以下几种:

      1)从本地系统直接加载

2)通过网络下载.class文件

3)从zip,jar等归档文件中加载.class文件

4)从专有数据库中提取.class文件

5)将Java源文件动态编译为.class文件(服务器)

 

2.4 加载器(来自http://blog.csdn.net/cutesource/article/details/5904501

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

1)Bootstrap ClassLoader

  负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader

  负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader

  负责加载classpath中指定的jar包及目录中class

4)Custom ClassLoader

  属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader,加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类在所有ClassLoader只加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

三、 JVM三种预定义类加载器

    JVM预定义有三种类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类加载器:

1)引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。它负责将<Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

2)扩展类加载器(extensions class loader):该类加载器在此目录里面查找并加载 Java 类。扩展类加载器是由Sun的                            ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dirs指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器

3)系统类加载器(system class loader):系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它

四、 类加载器“双亲委派”机制

 4.1 “双亲委派”机制介绍

  在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和标准扩展类加载器为例作简单分析。

                                

                      图一 标准扩展类加载器继承层次图                                                                            图二 系统类加载器继承层次图

 

通过图一和图二我们可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

 //加载指定名称(包括包名)的二进制类型,供用户调用的接口
 public Class<?> loadClass(String name) throws ClassNotFoundException{//…}
 //加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是,这里的resolve参数不一定真正能达到解析的效果~_~),供继承用
 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}
 //findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
 protected Class<?> findClass(String name) throws ClassNotFoundException {//…}
 //定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以 无需覆写,直接调用就可以了)
 protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{//…}

  通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。既然这样,我们就可以通过分析java.lang.ClassLoader中的loadClass(String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

public Class<?> loadClass(String name) throws ClassNotFoundException {
  return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)  throws ClassNotFoundException {
   // 首先判断该类型是否已经被加载
   Class c = findLoadedClass(name);
   if (c == null) {
     //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
       try {
         if (parent != null) {
         //如果存在父类加载器,就委派给父类加载器加载
             c = parent.loadClass(name, false);
           } else {
        //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
             c = findBootstrapClass0(name);
           }
       }catch (ClassNotFoundException e) {
         // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
         c = findClass(name);
       }
   }
   if (resolve) {
       resolveClass(c);
   }
   return c;
}

  通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:

图三 类加载器默认委派关系图

上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

  public static void main(String[] args) {
     try {
      System.out.println(ClassLoader.getSystemClassLoader());       System.out.println(ClassLoader.getSystemClassLoader().getParent();       System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());    }
catch (Exception e) {    e.printStackTrace();   }   }

  (说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器

  代码输出如下:
  sun.misc.Launcher$AppClassLoader@197d257
  sun.misc.Launcher$ExtClassLoader@7259da
  null

  通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。我们还是借助于代码分析一下:

  我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

    protected ClassLoader() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器
        this.parent = getSystemClassLoader();
        initialized = true;
    }
    protected ClassLoader(ClassLoader parent) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        //强制设置父类加载器
        this.parent = parent;
        initialized = true;
    }

    我们再看一下ClassLoader抽象类中parent成员的声明:
    // The parent class loader for delegation
    private ClassLoader parent;

声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:

1.系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认 会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

2.扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

     现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?

图四 标准扩展类加载器和系统类加载器成员大纲视图

 

图五 扩展类加载器和系统类加载器公共父类成员大纲视图

  通过图四和图五可以看出,标准扩展类加载器和系统类加载器及其父类(java.net.URLClassLoader和java.security.SecureClassLoader)都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试。所以,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会做更深入的分析)。

   4.2 类加载双亲委派示例 

  以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在eclipse中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

    package classloader.test.bean;
    public class TestBean {
        public TestBean() {}
    }

  在现有当前工程中另外建立一测试类(ClassLoaderTest.java)内容如下:

测试一:
 public class ClassLoaderTest {
    public static void main(String[] args) {
        try {
            //查看当前系统类路径中包含的路径条目
            System.out.println(System.getProperty("java.class.path"));
            //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
            Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
            //查看被加载的TestBean类型是被那个类加载器加载的
            System.out.println(typeLoaded.getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 }
对应的输出如下:
    D:"DEMO"dev"Study"ClassLoaderTest"bin
    sun.misc.Launcher$AppClassLoader@197d257
(说明:当前类路径默认的含有的一个条目就是工程的输出目录)

测试二:
  将当前工程输出目录下的…/classloader/test/bean/TestBean.class打包进test.jar剪贴到< Java_Runtime_Home >/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:
  D:"DEMO"dev"Study"ClassLoaderTest"bin
  sun.misc.Launcher$ExtClassLoader@7259da
  对比测试一和测试二,我们明显可以验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。

测试三:
  将test.jar拷贝一份到< Java_Runtime_Home >/lib下,运行测试代码,输出如下:
  D:"DEMO"dev"Study"ClassLoaderTest"bin
  sun.misc.Launcher$ExtClassLoader@7259da

  测试三和测试二输出结果一致。那就是说,放置到< Java_Runtime_Home >/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除< Java_Runtime_Home >/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

五、java程序动态扩展方式 

  Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。

  运行时动态扩展java应用程序有如下两个途径:

5.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注册驱动程序,这样才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加载.

5.2    用户自定义类加载器

  通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看):

1)首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2

2)委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,整个虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3

3)调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种类不止一种)。

(说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下)

六、常见问题分析

  6.1 由不同的类加载器加载的指定类型还是相同的类型吗?

  在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间我们可以用两个自定义类加载器去加载某自定义类型(注意,不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。

  6.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();

  6.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());

  本机对应输出如下:

sun.misc.Launcher$AppClassLoader@197d257

  所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:

  即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类

  1. <Java_Runtime_Home>/lib下的类

  2. < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类

  3. 当前工程类路径下或者由系统变量java.class.path指定位置中的类

 

  6.4 在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

  JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:

  即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。

  说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5

 

  6.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 {
     returnthis.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) 正确设置父类加载器

  通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。

     3) 保证findClass(String )方法的逻辑正确性

    事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

 

  6.6 如何在运行时判断系统类加载器能加载哪些路径下的类?

  一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到;

  二是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 , System.getProperty("java.class.path")

 

  6.7    如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

方法之一:
try {
  URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
  for (int i = 0; i < extURLs.length; i++) {
    System.out.println(extURLs[i]);
   }
} catch (Exception e) {//…}
//本机对应输出如下:
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar

 七、转载说明

   第二节,转自:http://blog.csdn.net/gjanyanlig/article/details/6818655

   第四~第六节,转自:http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html

 

 

posted @ 2016-09-08 18:08  小杜比亚  阅读(...)  评论(...编辑  收藏