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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.*;

public class MyClassLoader extends ClassLoader {
private String name;
private String path;

public MyClassLoader(String name, String path) {
this.name = name;
this.path = path;
}

@Override
public Class<?> findClass (String name) throws ClassNotFoundException {
byte[] bytes = loadClassData (name);
return defineClass (name, bytes, 0, bytes.length);
}

private byte[] loadClassData (String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
in = new FileInputStream(new File(name));
int i;
while ((i = in.read ()) != -1){
byteArrayOutputStream.write (i);
}
} catch (Exception e) {
e.printStackTrace ();
} finally {
try {
in.close ();
byteArrayOutputStream.close ();
} catch (IOException e) {
e.printStackTrace ();
}
}
return byteArrayOutputStream.toByteArray ();
}
}

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

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

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

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

mark

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

什么是双亲委派机制

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

mark

双亲委派机制工作过程:

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

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

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

4、重复步骤 3;

mark

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

1
2
//return null if not found
private 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 驱动的一些对象,而不是延迟加载。