Java使用类加载器来装载字节码到内存,以便后续用来创建对象调用方法等。就目前的JVM,要说这个ClassLoader,先要说到它的委托模型(有人将parent译作双亲,双亲委派模型,窃以为,很不准确,原因在说完这个委托模型之后讲)。何为委托模型?java.lang.ClassLoader有这样的描述:
每个 ClassLoader 实例都有一个相关的父类加载器。
需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机的内置类加载器(称为 "bootstrap class loader")本身没有父类加载器,但是可以将它用作 ClassLoader 实例的父类加载器。
开始说“每个 ClassLoader 实例都有一个相关的父类加载器”,后面又说“虚拟机的内置类加载器(称为 “bootstrap class loader”)本身没有父类加载器”,这不是自行矛盾吗!其实不然,前面说的“每个 ClassLoader 实例”指的是每个java.lang.ClassLoader(该类是抽象类)子类的对象。而bootstrap class loader(后面叫它引导类加载器)是jvm内部由c++实现的,并不继承java.lang.ClassLoader类,所以它不属于“ClassLoader 实例”,也没有办法在Java代码中获取到它。
从API描述中还可以得到的信息是,1、引导类装载器没有父类装载器,虽然不是ClassLoader的实例,但是可以作为其它ClassLoader实例的父类加载器;2、 每个java.lang.ClassLoader子类的对象都关联着一个父类加载器。也就是说这委托模型一种树状的模型,一个ClassLoader子类有且只有一个父类加载器,多个ClassLoader子类的父类加载器可以是同一个。这也是为什么我前面说“双亲委派模型”这种说法很不准确的原因。
类加载器可以分为两类:一是引导类装载器(c++实现,非ClassLoader的实例,用于加载java类库中的类);二是自定义类装载器(即所有继承了java.lang.ClassLoader的类加载器,它们本身是由引导类装载器装载进jvm的).在现在的JRE中,已经自带了一些自定义类加载器,常见且有名的有:扩展类装载器(其父类加载器为引导类加载器,用于加载jre的lib/ext目录中的类),系统类加载器(其父类加载器为扩展类加载器,用于加载classpath中的类)。可以通过一段小代码来了解:
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println( "系统类装载器:" + systemClassLoader); ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println( "系统类装载器的父类加载器——扩展类加载器:" + extClassLoader); ClassLoader bootClassLoader = extClassLoader.getParent(); System.out.println( "扩展类加载器的父类加载器——引导类加载器:" + bootClassLoader); |
运行结果如下:
系统类装载器:sun.misc.Launcher$AppClassLoader@1b000e7
系统类装载器的父类加载器——扩展类加载器:sun.misc.Launcher$ExtClassLoader@b76fa
扩展类加载器的父类加载器——引导类加载器:null
观察ClassLoader类的构造方法可以发现,可以显式指定父类加载器,也可以使用默认的形式。有些文章或资料上写到“默认的父类加载器为系统类加载器”,“默认的父类加载器为引导类加载器”,这里我又觉得含糊不清了,为什么?默认,这里可以有两种理解:一、使用ClassLoader() 不带参数的构造方法时其父类加载器是什么。二、使用ClassLoader(ClassLoader parent)这种传入null作为参数的构造方法时其父类加载器是什么。这两种“默认情况”是不一样的。当使用ClassLoader(ClassLoader parent)传入null的时候,其父类加载器是引导类加载器(当然,也可以将null理解成引导类加载器);当使用没有参数的ClassLoader()时,其父类加载器一般为系统类装载器,这个构造方法等价于ClassLoader(ClassLoader.getSystemClassLoader()),前面说这种方式“其父类加载器一般为系统类装载器”,是因为getSystemClassLoader方法是有可能返回null的,具体参见getSystemClassLoader的API文档。
这个委托模型是如何工作的呢?假设有以下模型,A→B→System(系统类加载器)→Ext(扩展类加载器)→Boot(引导类加载器),箭头那边是非箭头边的父类加载器,B是A的父类加载器,系统类加载器是B的父类加载器,以此类推。当让加载器A去加载一个类(假设这个类是C,位于在classpath中)的时候,A并不先自己去加载这个类,而是委托给其父类加载器,父类加载器执行同样的动作,直到没有父类加载器为止(一般是到了引导类加载器),如果该模型的顶端那个类加载器没办法加载指定的类,就会回退到Ext,Ext发现其也不能装载classpath中的类,就继续回退到System,这时,System发现其能装载指定的类了。在这个过程中,真正装载类的那个类装载器称作定义类装载器,导致类被加载的加载器称为初始类装载器,这个例子中,A,B,System都是初始类装载器,System是定义类装载器。定义类装载器是一个特殊的初始类装载器,而Ext,Boot既不是定义类装载器也不是初始类装载器。
每个加载器都有一个命名空间,所谓的命名空间就是加载器维护了一张表,表的内容为其作为初始类装载器所加载的类,也就是说,如加载器L,不管类是不是由L装载进jvm的,只要是L的父类加载器路径中的某个加载器装载的,L都会将这个装载的类记录下来,下次再使用L加载同样的类时,就会返回这个已经装载过的类。在上面的例子中,A,B,System的命名空间中都包含类C,下次B或者System需要装载C的时候,就会直接返回这个已经装载的类。
这样一来,不难发现,类的全限定名并不能唯一确定jvm中装载的类,还要加上装载该类的定义类装载器,在java.lang.Class中有方法getClassLoader,它返回的就是装载该类的定义类加载器,如果是引导类加载器,返回的是null。那么,不同的装载器装载同一个类后,他们的对象能够赋值给对方的引用吗?来看个例子(如无特别说明,这里的例子都只能运行于eclipse中,且eclipse中存放编译后的class文件的目录叫做bin,要运行于其它环境,需要修改程序):
先来一个Person类:
package com.ticmy.classloader; public class Person {} |
再看测试代码:
package com.ticmy.classloader; import java.net.URL; import java.net.URLClassLoader; public class TestClassLoader { public static void main(String[] args) throws Exception { String url = "file://" + System.getProperty( "user.dir" ).replaceAll( "\\\\" , "/" ) + "/bin/" ; System.out.println(url); ClassLoader c1 = new URLClassLoader( new URL[]{ new URL(url)}, null ); System.out.println( "c1的父类加载器: " + c1.getParent()); System.out.println( "SystemClassLoader: " + ClassLoader.getSystemClassLoader()); Class<?> class1 = c1.loadClass( "com.ticmy.classloader.Person" ); Object o = class1.newInstance(); System.out.println( "Person:" + o); System.out.println( "Test的定义类装载器: " + TestClassLoader. class .getClassLoader()); System.out.println( "Test中直接使用Person使用的ClassLoader: " + Person. class .getClassLoader()); System.out.println( "自定义装载器装载Person的定义类加载器: " + o.getClass().getClassLoader()); Person p = (Person)o; } } |
运行结果如下:
file://E:/workSpace/test/bin/
c1的父类加载器: null
SystemClassLoader: sun.misc.Launcher$AppClassLoader@1b000e7
Person:com.ticmy.classloader.Person@19e8f17
Test的定义类装载器: sun.misc.Launcher$AppClassLoader@1b000e7
Test中直接使用Person使用的ClassLoader: sun.misc.Launcher$AppClassLoader@1b000e7
自定义装载器装载Person的定义类加载器: java.net.URLClassLoader@15093f1
Exception in thread "main" java.lang.ClassCastException: com.ticmy.classloader.Person cannot be cast to com.ticmy.classloader.Person
at com.ticmy.classloader.TestClassLoader.main(TestClassLoader.java:21)
new URLClassLoader的时候,其父加载器传的是null,也就是说其父类加载器是引导类加载器。url所指的路径,既属于系统类加载器寻找的classpath(所以可以直接在程序中new Person),又属于c1查找类的路径。程序制定c1去装载com.ticmy.classloader.Person,首先会委派给其父加载器——引导类加载器——去装载,引导类加载器发现自己找不到指定的类,于是回退到c1自身去装载这个类,而c1能找到这个类,所以class1的定义类加载器就是c1。而在程序中直接写Person p = …的这种形式,Person是存在于TestClassLoader常量池的符号引用中的,当需要用到Person的时候,会使用装载TestClassLoader类的装载器去装载,所以,直接写Person p = …其装载器为系统类装载器,也就是hotspot中的sun.misc.Launcher$AppClassLoader。这样,创建一个由class1装载的Person的对象转换成由系统类装载器装载的Person的引用,就报ClassCastException,无法转换,因为它们已经不属于同一个类了。
若是将com.ticmy.classloader.Person换成java.lang.String呢?
package com.ticmy.classloader; import java.net.URL; import java.net.URLClassLoader; public class TestClassLoader { public static void main(String[] args) throws Exception { String url = "file://" + System.getProperty( "user.dir" ).replaceAll( "\\\\" , "/" ) + "/bin/" ; System.out.println(url); ClassLoader c1 = new URLClassLoader( new URL[]{ new URL(url)}, null ); System.out.println( "c1的父类加载器: " + c1.getParent()); System.out.println( "SystemClassLoader: " + ClassLoader.getSystemClassLoader()); Class<?> class1 = c1.loadClass( "java.lang.String" ); Object o = class1.newInstance(); System.out.println( "Person:" + o); System.out.println( "Test的定义类装载器: " + TestClassLoader. class .getClassLoader()); System.out.println( "Test中直接使用Person使用的ClassLoader: " + Person. class .getClassLoader()); System.out.println( "自定义装载器装载Person的定义类加载器: " + o.getClass().getClassLoader()); String p = (String)o; } } |
运行之后发现没有报错。这是因为c1去装载java.lang.String的时候委托给引导类加载器装载,引导类加载器是可以加载的,其生成的class1的类是java.lang.String,定义类加载器是引导类加载器。而直接写的形式中,由系统类加载器发起装载请求,系统类加载器将其委托给扩展类加载器,扩展类加载器再委托给引导类加载器,最终引导类加载器可以加载java.lang.String(已经加载过就直接返回加载过的)。这样直接写的String的类名是java.lang.String,定义类加载器也是引导类加载器,所以由c1发起装载的java.lang.String的对象是可以转换成由系统类加载器发起装载的java.lang.String的引用的。
前面new URLClassLoader传的是null参数,如果使用无参构造呢?
package com.ticmy.classloader; import java.net.URL; import java.net.URLClassLoader; public class TestClassLoader { public static void main(String[] args) throws Exception { String url = "file://" + System.getProperty( "user.dir" ).replaceAll( "\\\\" , "/" ) + "/bin/" ; System.out.println(url); ClassLoader c1 = new URLClassLoader( new URL[]{ new URL(url)}); System.out.println( "c1的父类加载器: " + c1.getParent()); System.out.println( "SystemClassLoader: " + ClassLoader.getSystemClassLoader()); Class<?> class1 = c1.loadClass( "com.ticmy.classloader.Person" ); Object o = class1.newInstance(); System.out.println( "Person:" + o); System.out.println( "Test的定义类装载器: " + TestClassLoader. class .getClassLoader()); System.out.println( "Test中直接使用Person使用的ClassLoader: " + Person. class .getClassLoader()); System.out.println( "自定义装载器装载Person的定义类加载器: " + o.getClass().getClassLoader()); Person p = (Person)o; } } |
运行之后也是没有问题的。前面已经说到两个默认,这种“默认”的父类加载器就是系统类加载器,而系统类加载器是可以加载Person的,因此,两个不同的类加载器发起的对Person的装载,最后,他们都是由同一个类加载器装载的,也就说,两种情况下,Person的定义类加载器都是系统类加载器,故,他们可以转换。