浅谈ClassLoader

来源:互联网 发布:文明5 for mac 汉化 编辑:程序博客网 时间:2024/06/05 03:04

2012-8-2 09:53|发布者: benben|查看: 1632|评论: 0

摘要: JAVA启动后,是经过JVM各级ClassLoader来加载各个类到内存。为了更加了解加载过程,我通过分析和写了一个简单的ClassLoader来粗浅的分析它的原理。JVM的ClassLoader分三层,分别为Bootstrap ClassLoader,Extension ...

JAVA启动后,是经过JVM各级ClassLoader来加载各个类到内存。为了更加了解加载过程,我通过分析和写了一个简单的ClassLoader来粗浅的分析它的原理。

JVMClassLoader分三层,分别为Bootstrap ClassLoaderExtension ClassLoaderSystem ClassLoader,他们不是类继承的父子关系,是逻辑上的上下级关系。

Bootstrap ClassLoader是启动类加载器,它是用C++编写的,从%jre%/lib目录中加载类,或者运行时用-Xbootclasspath指定目录来加载。

Extension ClassLoader是扩展类加载器,从%jre%/lib/ext目录加载类,或者运行时用-Djava.ext.dirs制定目录来加载。

System ClassLoader,系统类加载器,它会从系统环境变量配置的classpath来查找路径,环境变量里的.表示当前目录,是通过运行时-classpath-Djava.class.path指定的目录来加载类。

 

一般自定义的Class Loader可以从java.lang.ClassLoader继承,不同classloader加载相同的类,他们在内存也不是相等的,即它们不能互相转换,会直接抛异常。java.lang.ClassLoader的核心加载方法是loadClass方法,如:

   protectedsynchronized Class<?> loadClass(String name,boolean resolve)

        throws ClassNotFoundException

    {

        // First, check if the class has already been loaded

         Class c = findLoadedClass(name);

        if (c ==null) {

            try {

                  if (parent !=null) {

                       c =parent.loadClass(name,false);

                   }else {

                       c = findBootstrapClass0(name);

                   }

             }catch (ClassNotFoundException e) {

                // If still not found, then invoke findClass in order

                // to find the class.

                 c = findClass(name);

             }

         }

        if (resolve) {

             resolveClass(c);

         }

        return c;

    }

通过上面加载过程,我们能知道JVM默认是双亲委托加载机制,即首先判断缓存是否有已加载的类,如果缓存没有,但存在父加载器,则让父加载器加载,如果不存在父加载器,则让Bootstrap ClassLoader去加载,如果父类加载失败,则调用本地的findClass方法去加载。

可以通过下面三条语句,输入现在加载的各个classloader的加载路径:

        System.out.println("sun.boot.class.path:" + System.getProperty("sun.boot.class.path"));   

        System.out.println("java.ext.dirs:" + System.getProperty("java.ext.dirs"));   

        System.out.println("java.class.path:" +System.getProperty("java.class.path"));

        ClassLoader cl = Thread.currentThread().getContextClassLoader();//ClassLoader.getSystemClassLoader()

        System.out.println("getContextClassLoader:" +cl.toString());

        System.out.println("getContextClassLoader.parent:" +cl.getParent().toString());

        System.out.println("getContextClassLoader.parent2:" +cl.getParent().getParent());

输出结果为:

sun.boot.class.path:C:\Program Files\Java\jre7\lib\resources.jar;C:\Program Files\Java\jre7\lib\rt.jar;C:\Program Files\Java\jre7\lib\sunrsasign.jar;C:\Program Files\Java\jre7\lib\jsse.jar;C:\Program Files\Java\jre7\lib\jce.jar;C:\Program Files\Java\jre7\lib\charsets.jar;C:\Program Files\Java\jre7\classes

java.ext.dirs:C:\Program Files\Java\jre7\lib\ext;C:\Windows\Sun\Java\lib\ext

java.class.path:E:\MyProjects\workspace\TestConsole\bin

getContextClassLoader:sun.misc.Launcher$AppClassLoader@19dbc3b

getContextClassLoader.parent:sun.misc.Launcher$ExtClassLoader@b103dd

getContextClassLoader.parent2:null

从上面的运行结果可以看出逻辑上的层级继承关系。双亲委托机制的作用是防止系统jar包被本地替换,因为查找方法过程都是从最底层开始查找。因此,一般我们自定义的classloader都需要采用这种机制,我们只需要继承java.lang.ClassLoader实现findclass即可,如果需要更多控制,自定义的classloader就需要重写loadClass方法了,比如tomcat的加载过程,这个比较复杂,可以通过其他文档资料查看相关介绍。

各个ClassLoader加载相同的类后,他们是不互等的,这个当涉及多个ClassLoader,并且有通过当前线程上线文获取ClassLoader后转换特别需要注意,可以通过线程的setContextClassLoader设置一个ClassLoader线程上下文,然后再通过Thread.currentThread().getContextClassLoader()获取当前线程保存的Classloader。但是自定义的类文件,放到Bootstrap ClassLoader加载目录,是不会被Bootstrap ClassLoader加载的,因为作为启动类加载器,它不会加载自己不熟悉的jar包的,并且类文件必须打包成jar包放到加载器加载的根目录,才可能被扩展类加载器所加载。

                                                      

下面我自定义一个简单的classloader

publicclass TestClassLoaderextends ClassLoader {

   //定义文件所在目录  

   privatestaticfinal StringDEAFAULTDIR="E:\\MyProjects\\workspace\\TestConsole\\bin\\";

   

   public Class<?> findClass(String name)throws ClassNotFoundException {

       byte[] b =null;  

       try {  

            b = loadClassData(GetClassName(name));  

        }catch (Exception e) {  

            e.printStackTrace();  

        } 

       return defineClass(name, b, 0, b.length);    

    }   

   @Override

   protectedsynchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException {

       if(name.startsWith("java.")){try {

            returnsuper.loadClass(name,false);    

        }catch (ClassNotFoundException e) {

            e.printStackTrace();  

        }

        }

      byte[] b =null;  

      try {  

           b = loadClassData(GetClassName(name));  

       }catch (Exception e) {  

           e.printStackTrace();  

       } 

      return defineClass(name, b, 0, b.length);  

    }  

   privatebyte[] loadClassData(String filepath)throws Exception {  

       int n =0;  

        BufferedInputStream br =new BufferedInputStream(  

                       new FileInputStream(  

                   new File(filepath)));  

        ByteArrayOutputStream bos=new ByteArrayOutputStream();  

           while((n=br.read())!=-1){  

                bos.write(n);  

            }  

            br.close();  

       return bos.toByteArray();  

    }

   publicstatic String GetClassName(String name){

       returnDEAFAULTDIR+name.replace('.','/')+".class";

    }

}

这个自定义的ClassLoader重写了loadclass方法,但不用默认的双亲委托,比如java.lang包下面的都无法解析,这里我简单的判断如果是java.开始的包则用父类去解析,能简单的满足双亲委托机制,但是其他相关非系统类加载也没有用父类加载了。

测试代码如:

        TestClassLoader liuloader =new TestClassLoader();

        Myrunnerrunner =new Myrunner();

       runner.setContextClassLoader(liuloader);

       runner.start();

Myrunner是我自定义继承自Thread的线程,通过设置线程上下文的classloader后,线程内部测试代码如:

        ClassLoader cl1 = Thread.currentThread().getContextClassLoader();

        System.out.println(cl1);

它将会输出:

com.liu.ClassLoader.TestClassLoader@347cdb,说明已经为当前线程上下文设置了自定义的Classloader了,如果这个线程内部通过这个classloader加载一个类,再转换成当前的类,如代码:

   Class c = cl1.loadClass("com.liu.ClassLoader.TestLoader2");    TestLoader2 tloader = (TestLoader2)c.newInstance();

则为抛java.lang.ClassCastException异常:com.liu.ClassLoader.TestLoader2 cannot be cast to com.liu.ClassLoader.TestLoader2

因为cl1当前是 TestClassLoader加载的,而这个TestLoader2的类还是默认由AppClassLoader加载,因此它们不能隐式转换,Classloader加载相同的类,内存认为它们是没有关系的对象。

如果把我自定义的TestClassLoader里的LoadClass方法去掉,则采用了双亲委托机制,这样我们除了指定的类以外,其他都会优先用父类来加载。这样可以解决刚才的java.lang.ClassCastException异常问题,为加载的对象建立一个抽象父类,自定义的Classloader负责加载子类,父类统一交给AppClassLoader或父加载器来加载,这样线程内部可以使用类试:

       Class c = cl1.loadClass("com.liu.ClassLoader.TestLoader2");

        BaseTest tloader = (BaseTest)c.newInstance();

BaseTestTestLoader2的父类,因为BaseTest都是AppClassLoader或父加载器加载的,因此可以达到成功隐式转换的目的。

对于Tomcat等几个处理的Classloader都是自定义并重写了loadclass方法,内部会更复杂处理。

 

作为一个新手,本文是自己对javaclassloader的一个粗略理解,如有问题,请及时指正。


------------------------------------------------------------------------------------------------------------------------------------

加载类的过程

  在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass 来实现的;而启动类的加载过程是通过调用 loadClass 来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer 引用了类 com.example.Inner,则由类 com.example.Outer 的定义加载器负责启动类 com.example.Inner 的加载过程。

  方法 loadClass() 抛出的是 java.lang.ClassNotFoundException 异常;方法 defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常。

  类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass 方法不会被重复调用。