[认知]ClassLoader 认知一二三


  曾经深入了解过ClassLoader,但一直没有时间去总结,现在以参考+自我认识的方式总结一下ClassLoader的相关内容,不能保证100%全,但目前对于一个毛头孩子来说,enough!

  首先就要知道ClassLoader是用来干什么的,顾名思义,它就是用来加载Class文件到JVM,以供程序使用的。我们知道,java程序可以动态加载类定义,而这个动态加载的机制就是通过ClassLoader来实现的,所以可想而知ClassLoader的重要性如何。

  看到这里,可能有的朋友会想到一个问题,那就是既然ClassLoader是用来加载类到JVM中的,那么ClassLoader又是如何被加载呢?难道它不是java的类?

  没有错,在这里确实有一个ClassLoader不是用java语言所编写的,而是JVM实现的一部分,这个ClassLoader就是bootstrap classloader(启动类加载器),这个ClassLoader在JVM运行的时候加载java核心的API以满足java程序最基本的需求,其中就包括用户定义的ClassLoader,这里所谓的用户定义是指通过java程序实现的ClassLoader,一个是ExtClassLoader,这个ClassLoader是用来加载java的扩展API的,也就是/lib/ext中的类,一个是AppClassLoader,这个ClassLoader是用来加载用户机器上CLASSPATH设置目录中的Class的,通常在没有指定ClassLoader的情况下,程序员自定义的类就由该ClassLoader进行加载。

  当运行一个程序的时候,JVM启动,运行bootstrap classloader,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。

  接下来将讲解一下ClassLoader加载的方式,这里就不得不讲一下ClassLoader在这里使用了双亲委托模式进行类加载。

  每一个自定义ClassLoader都必须继承ClassLoader这个抽象类,而每个ClassLoader都会有一个parent ClassLoader,我们可以看一下ClassLoader这个抽象类中有一个getParent()方法,这个方法用来返回当前ClassLoader的parent,注意,这个parent不是指的被继承的类,而是在实例化该ClassLoader时指定的一个ClassLoader,如果这个parent为null,那么就默认该ClassLoader的parent是bootstrap classloader,这个parent有什么用呢?

  我们可以考虑这样一种情况,假设我们自定义了一个ClientDefClassLoader,我们使用这个自定义的ClassLoader加载java.lang.String,那么这里String是否会被这个ClassLoader加载呢?事实上java.lang.String这个类并不是被这个ClientDefClassLoader加载,而是由bootstrap classloader进行加载,为什么会这样?实际上这就是双亲委托模式的原因,因为在任何一个自定义ClassLoader加载一个类之前,它都会先委托它的父亲ClassLoader进行加载,只有当父亲ClassLoader无法加载成功后,才会由自己加载,在上面这个例子里,因为java.lang.String是属于java核心API的一个类,所以当使用ClientDefClassLoader加载它的时候,该ClassLoader会先委托它的父亲ClassLoader进行加载,上面讲过,当ClassLoader的parent为null时,ClassLoader的parent就是bootstrap classloader,所以在ClassLoader的最顶层就是bootstrap classloader,因此最终委托到bootstrap classloader的时候,bootstrap classloader就会返回String的Class。

  至于为什么要使用这种双亲委托模式呢?

  第一个原因就是因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

  第二个原因就是考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。

   上面对ClassLoader的加载机制进行了大概的介绍,接下来不得不在此讲解一下另外一个和ClassLoader相关的类,那就是Class类,每个被ClassLoader加载的class文件,最终都会以Class类的实例被程序员引用,我们可以把Class类当作是普通类的一个模板,JVM根据这个模板生成对应的实例,最终被程序员所使用。

   我们看到在Class类中有个静态方法forName,这个方法和ClassLoader中的loadClass方法的目的一样,都是用来加载class的,但是两者在作用上却有所区别。
Class<?> loadClass(String name)
Class<?> loadClass(String name, boolean resolve)
我们看到上面两个方法声明,第二个方法的第二个参数是用于设置加载类的时候是否连接该类,true就连接,否则就不连接。

      说到连接,不得不在此做一下解释,在JVM加载类的时候,需要经过三个步骤,装载、连接、初始化。装载就是找到相应的class文件,读入JVM,初始化就不用说了,最主要就说说连接。

      连接分三步,第一步是验证class是否符合规格,第二步是准备,就是为类变量分配内存同时设置默认初始值,第三步就是解释,而这步就是可选的,根据上面loadClass方法的第二个参数来判定是否需要解释,所谓的解释根据《深入JVM》这本书的定义就是根据类中的符号引用查找相应的实体,再把符号引用替换成一个直接引用的过程。有点深奥吧,呵呵,在此就不多做解释了,想具体了解就翻翻《深入JVM吧》,呵呵,再这样一步步解释下去,那就不知道什么时候才能解释得完了。

      我们再来看看那个两个参数的loadClass方法,在JAVA API 文档中,该方法的定义是protected,那也就是说该方法是被保护的,而用户真正应该使用的方法是一个参数的那个,一个参数的loadclass方法实际上就是调用了两个参数的方法,而第二个参数默认为false,因此在这里可以看出通过loadClass加载类实际上就是加载的时候并不对该类进行解释,因此也不会初始化该类。而Class类的forName方法则是相反,使用forName加载的时候就会将Class进行解释和初始化,forName也有另外一个版本的方法,可以设置是否初始化以及设置ClassLoader,在此就不多讲了。

      不知道上面对这两种加载方式的解释是否足够清楚,就在此举个例子吧,例如JDBC DRIVER的加载,我们在加载JDBC驱动的时候都是使用的forName而非是ClassLoader的loadClass方法呢?我们知道,JDBC驱动是通过DriverManager,必须在DriverManager中注册,如果驱动类没有被初始化,则不能注册到DriverManager中,因此必须使用forName而不能用loadClass。  

 

ClassLoader类介绍

ClassLoader类位于java.lang包中,直接继承自Object,是一个抽象类。

两个构造函数(protected)

一个带parent的参数,一个无参(默认用System ClassLoader作为其parent)。

静态方法

public static ClassLoader getSystemClassLoader()

获取System ClassLoader。可以通过设置系统属性:java.system.class.loader的值以修改该函数的返回类型(System.setProperty, System.getProperty)。

public static URL getSystemResource(String name)

Resource是指数据,如图片、文本、音频等。我的理解Resource就是指在对应ClassLoader搜索路径(目录和jar文件内部)下的任何数据,包括文件和目录。那么该函数就是通过名字(目录名或文件名)返回资源所对应的URL值。(注:名字和返回中的目录分隔符都是用“/”来表示)。该函数的搜索顺序和.class文件的搜索顺序是一样的,所不同的是,用户自定义的ClassLoader可以通过重写public URL findResource(String name)方法来扩展该函数的搜索范围。

public static Enumeration<URL> getSystemResources(String name)

同getSystemResource,只是它会返回多个URL的集合,用户自定义的ClassLoader可以通过public URL findResources(String name)来扩展该方法。 

public static InputStream getSystemResourceAsStream(String name)

该函数内部通过调用getSystemResource()方法获取资源的URL地址,然后调用URL.openStream()方法获取资源的InputStream。

public方法

public Class<?> loadClass(String name) throws ClassNotFoundException

protected synchronized Class<?> loadClass(String name,boolean resolve)    throws ClassNotFoundException

通过类名加载类,在ClassLoader中,会缓存已加载的Class实例,因而该方法首先查询缓存中是否已经存在该类的Class实例。该方法实现了ClassLoader加载类的算法。按JDK的表述,resolve是指链接(link)的意思,但是link又是指什么呢?这个我还清楚。用户自定义子类可以通过findClass()方法来扩展该方法。

public URL getResource(String name)

类似静态方法的getResource()方法。

public Enumeration<URL> getResources(String name)

类似静态方法的getResources()方法。

public final ClassLoader getParent()

如方法名。

public InputStream getResourceAsStream(String name)

类似静态方法的getResourceAsStream()方法

protected方法

protected URL findResource(String name)

getResource()的扩展方法,提供给子类重写。

protected Enumeration<URL> findResources(String name) throws IOException

getResources()的扩展方法,提供给子类重写。

protected Class<?> findClass(String name) throws ClassNotFoundException

loadClass()函数的扩展方法,该方法会在所有parent ClassLoader没有找到相应的类定义的时候调用,因而该方法只需要实现当前ClassLoader中需要额外搜索的路径即可。

protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,ProtectionDomain protectionDomain) throws ClassFormatError

protected final Class<?> defineClass(String name,byte[] b,int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError

protected final Class<?> defineClass(String name,byte[] b,int off, int len) throws ClassFormatError

该方法用于将字节数组转化为相应的Class对象,在该Class对象使用前,该对象必须已经被解析了(resolved or linked,同样,什么是解析或链接?)。可以指定ProtectionDomain,也可以使用默认的ProtectionDomain。子类在实现findClass()的时候可以调用该方法以将找到的.class二进制内容转化为Class实例。

  嗯,大概就是这些.

      下面呢,关于自定义ClassLoader,很多时候人们会使用一些自定义的ClassLoader ,而不是使用系统的Class Loader。大多数时候人们这样做的原因是,他们在编译时无法预知运行时会需要那些Class。特别是在那些appserver中,比如tomcat,Avalon-phonix,Jboss中。或是程序提供一些plug-in的功能,用户可以在程序编译好之后再添加自己的功能,比如ant, jxta-shell等。定制一个ClassLoader很简单,一般只需要理解很少的几个方法就可以完成。


  一个最简单的自定义的ClassLoader从ClassLoader类继承而来。这里我们要做一个可以在运行时指定路径,加载这个路径下的class的ClassLoader。通常我们使用ClassLoader.loadClass(String):Class方法,通过给出一个类名,就会得到一个相应的Class实例。因此只要小小的改动这个方法,就可以实现我们的愿望了,下面是例子。

 

 1 package classloader;
 2 
 3 import java.io.ByteArrayOutputStream;
 4 import java.io.File;
 5 import java.io.FileInputStream;
 6 
 7 /**
 8  * 
 9  * 一、ClassLoader加载类的顺序
10  *  1.调用 findLoadedClass(String) 来检查是否已经加载类。
11  *  2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。 
12  *  3.调用 findClass(String) 方法查找类。
13  * 二、实现自己的类加载器
14  *     1.获取类的class文件的字节数组
15  *     2.将字节数组转换为Class类的实例
16  * 
17  * 
18  * @author lei 2011-9-1
19  */
20 public class ClassLoaderTest {
21 
22     public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
23         //新建一个类加载器
24         MyClassLoader cl = new MyClassLoader("myClassLoader");
25         //加载类,得到Class对象
26         Class<?> clazz = cl.loadClass("classloader.Animal");
27         //得到类的实例
28         Animal animal=(Animal) clazz.newInstance();
29         animal.say();
30     }
31 
32 }
33 class Animal{
34     public void say(){
35         System.out.println("hello world!");
36     }
37 }
38 class MyClassLoader extends ClassLoader {
39     //类加载器的名称
40     private String name;
41     //类存放的路径
42     private String path = "E:\\workspace\\Algorithm\\src";
43 
44     MyClassLoader(String name) {
45         this.name = name;
46     }
47     
48     MyClassLoader(ClassLoader parent, String name) {
49         super(parent);
50         this.name = name;
51     }
52     /**
53      * 重写findClass方法
54      */
55     @Override
56     public Class<?> findClass(String name) {
57         byte[] data = loadClassData(name);
58         return this.defineClass(name, data, 0, data.length);
59     }
60     public byte[] loadClassData(String name) {
61         try {
62             name = name.replace(".", "//");
63             FileInputStream is = new FileInputStream(new File(path + name + ".class"));
64             ByteArrayOutputStream baos = new ByteArrayOutputStream();
65             int b = 0;
66             while ((b = is.read()) != -1) {
67                 baos.write(b);
68             }
69             return baos.toByteArray();
70         } catch (Exception e) {
71             e.printStackTrace();
72         }
73         return null;
74     }
75 
76 }

 

  差不多就这样了, 感谢原作者的启发, 文笔不好, 勿怪~ 

参考自 itlab刀idcquan刀com/Java/others/14215刀html

    www刀iteye刀com/topic/83978

    blog刀csdn刀net/ymeng_bupt/article/details/6843998

posted @ 2014-10-28 09:31  feed_bird  阅读(121)  评论(0编辑  收藏  举报