闲聊ClassLoader的父加载器

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的定义类加载器都是系统类加载器,故,他们可以转换。

你可能感兴趣的:(Java)