Java的ClassLoader

mark

ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件。所有的Class都是由ClassLoader 进行加载的。ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。

ClassLoader的种类

  • BootStrapClassLoader:C++编写,这个类加载器负责将\lib目录下的类库加载到虚拟机内存中,用来加载Java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被Java程序直接调用,代码是使用C++编写的,它虚拟机自身的一部分。
  • ExtClassLoader:Java编写,加载扩展库。 这个类加载器负责加载\lib\ext目录下的类库,用来加载Java的扩展库,开发者可以直接使用这个类加载器。
  • AppClassLoader:Java编写,加载程序所在目录的Class,这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的Java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。一般情况下这就是系统默认的类加载器。
  • 自定义ClassLoader:Java编写,定制化加载

mark

实现自己的ClassLoader

如何实现自己的类加载器呢?其实无非是把class文件转化成二进制流加载到JVM中:

mark

 1import java.io.*;
 2
 3public class MyClassLoader extends ClassLoader {
 4    private String name;
 5    private String path;
 6
 7    public MyClassLoader(String name, String path) {
 8        this.name = name;
 9        this.path = path;
10    }
11
12    @Override
13    public Class<?> findClass(String name) throws ClassNotFoundException {
14        byte[] bytes = loadClassData(name);
15        return defineClass(name, bytes, 0, bytes.length);
16    }
17
18    private byte[] loadClassData(String name) {
19        name = path + name + ".class";
20        InputStream in = null;
21        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
22        try {
23            in = new FileInputStream(new File(name));
24            int i;
25            while ((i = in.read()) != -1){
26                byteArrayOutputStream.write(i);
27            }
28        } catch (Exception e) {
29            e.printStackTrace();
30        } finally {
31            try {
32                in.close();
33                byteArrayOutputStream.close();
34            } catch (IOException e) {
35                e.printStackTrace();
36            }
37        }
38        return byteArrayOutputStream.toByteArray();
39    }
40}

我在桌面编写了简单的Demo.java,并编译成了class文件,也放在桌面的:

1public class Demo{
2	static{
3		System.out.println("Hello Demo!");
4	}
5}

于是我们可以开始测试自己写的类加载器了:

 1public class ClassLoaderTest {
 2    public static void main(String[] args) throws Exception{
 3        MyClassLoader classLoader = new MyClassLoader("Tim's ClassLoader", "C:\\Users\\15291\\Desktop\\");
 4        System.out.println(classLoader); //MyClassLoader@4554617c
 5        System.out.println(classLoader.getParent()); //sun.misc.Launcher$AppClassLoader@18b4aac2
 6        System.out.println(classLoader.getParent().getParent()); //sun.misc.Launcher$ExtClassLoader@74a14482
 7        System.out.println(classLoader.getParent().getParent().getParent()); //null
 8        Class<?> loaderClass = classLoader.loadClass("Demo"); //Hello Demo!
 9        loaderClass.newInstance();
10    }
11}

mark

由此也证明了ClassLoader之间的继承关系。

什么是双亲委派机制

这么多类加载器,那么它们之间是如何配合的呢?下面是双亲委派机制的模型图:

mark

双亲委派机制工作过程:

1、类加载器收到类加载的请求,如果已经加载过,则不需要再次加载;

2、如果未加载过,则把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;

3、启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。

4、重复步骤3;

mark

其实也可以看到这个递归调用流程的代码,可以看到findBootstrapClass是一个native方法

1// return null if not found
2private native Class<?> findBootstrapClass(String name);

那么这个native方法的实现在哪看呢? http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/beb15266ba1a/src/share/native/java/lang/ClassLoader.c

mark

最终调用了JVM_FindClassFromBootLoader这个方法,在这里暂且先不研究这个Native方法了。

为什么要使用双亲委派机制

首先明确一点是JVM如何认定两个对象同属于一个类型,必须同时满足下面两个条件: 1、都是用同名的类完成实例化的。

2、两个实例各自对应的同名的类的加载器必须是同一个。比如两个相同名字的类,一个是用系统加载器加载的,一个扩展类加载器加载的,两个类生成的对象将被JVM认定为不同类型的对象。

所以,为了系统类的安全,类似java.lang.Object这种核心类,JVM需要保证他们生成的对象都会被认定为同一种类型。即通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的,同时也就避免了多分同样的字节码加载。

能不能自己写个类叫java.lang.System?

当然可以,通过自定义类加载器的方式,只要让我们自己的类无法让父类加载器加载最终肯定就会让我们自定义的类加载器来加载这个类。

由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

类的加载方式(loadClass与forName)

1、new

使用new关键字去操作对象的时候会触发隐式加载,比如new Demo(),那么最终AppClassLoader就会加载这个Demo类

2、loadClass、forName

这种方式加做显式加载

那么loadClass与forName有什么区别呢?先来看看整个类的装载过程吧:

mark

在loadClass方法中,我们注意到一个boolean类型的参数叫做resolve,false即表示不执行链接的过程,即不解析这个类。

mark

再看看forName

mark

所以上述表明,使用forName显式加载可以选择是否执行初始化,如果只是传类名称的话默认是要执行初始化的,但是loadClass却没有链接和初始化的步骤,这种行为加做延迟加载,IOC为了提高启动速度就选择了使用延迟加载,在IOC容器中被广泛使用。forName什么时候用呢?比如使用MySQL驱动的时候,就是直接初始化MySQL驱动的一些对象,而不是延迟加载。