一、前言
平时小伙伴们在做业务开发的时候可能比较少去接触类加载器,但是如果你想要深入学习Tomcat这种开源项目的话,熟悉类加载的原理就是必须的了。
二、类加载器
类加载器,顾名思义,就是一个可以将java字节码加载为java.lang.Class实例的工具。
类加载器有以下两个特点:
1.动态加载:不需要在程序一开始运行的时候就去加载,而是在程序运行的过程中,动态地按需加载,.class字节码可以来源于各个地方,比如jar包、war包、网络中等等。
2.全盘负责:当一个类加载器去加载一个类时,这个类所依赖的、引用的其它所有的类都将由这个类加载器去加载,除非在程序中显示地指定另外一个类加载器加载。
一个类的唯一性是由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实力ID作为唯一标识,也就是PackageName+ClassName+ClassLoader Id),因此在一个运行程序中有可能存在两个包名和类名完全一致的类,但是如果这两个类不是由同一个ClassLoader加载的,就会被视为两个不同的类。比较两个类是否相等(包括Class对象的equals()、isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。
从实现方式上,类加载器可以分为两种:一种是启动类加载器,是由C++语言实现的,是虚拟机自身的一部分;另一种是继承于java.lang.ClassLoader的类加载器,包括扩展类加载器、应用程序类加载器以及自定义类加载器。
启动类加载器(Bootstrap ClassLoader):负责加载
我们写个代码来测试一下启动类加载器的打印:
为什么打印的不是“Bootstrap ClassLoader”而是 null 呢? 这是因为启动类加载器(Bootstrap ClassLoader)是由 C++ 实现的,而这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。
扩展类加载器(Extension ClassLoader):负责加载
我们写个代码来测试一下扩展类加载器的打印:
应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,由sun.misc.Launcher$App-ClassLoader实现。开发者可直接通过java.lang.ClassLoader中的getSystemClassLoader()方法获取应用程序类加载器,所以也可称它为系统类加载器。应用程序类加载器也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。在一个应用程序中,系统类加载器一般是默认类加载器。
应用程序类加载器是用来加载 classpath 也就是用户写的所有类的,接下来我们写代码测试一下应用程序类加载器的打印,代码如下:
三、双亲委派机制
双亲委派机制,可以用一句话来说表达:任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载。
双亲委派机制是java类加载器的一种工作模式,通过这种工作模式,java虚拟机将类文件加载到内存中,这样就保证了java程序能够正常的运行起来。JVM 并不是在启动时就把所有的.class文件都加载进来,而是程序在运行过程中用到了这个类才去加载。除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。
public abstract class ClassLoader {
//每个类加载器都有个父加载器
private final ClassLoader parent;
public Class> loadClass(String name) {
//查找一下这个类是不是已经加载过了
Class> c = findLoadedClass(name);
//如果没有加载过
if( c == null ){
//先委派给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class> defineClass(byte[] b, int off, int len){
...
}
}
从上面的代码可以得到几个关键信息:
- JVM 的类加载器是分层次的,它们有父子关系,而这个关系不是继承维护(不是java中的extend继承),而是组合,每个类加载器都持有一个 parent 字段(private final ClassLoader parent;),指向父加载器。(AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是BootstrapClassLoader,但是ExtClassLoader的parent=null。)
- defineClass 方法的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象。
- findClass 方法的主要职责就是找到.class文件,并且把.class文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。子类必须实现findClass 。
- loadClass 方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。
双亲委派机制原理:
- 如果一个类加载器收到了要加载某个类的请求,它要做的首要事情不是直接去加载,而是将这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
四、为什么要双亲委派
双亲委派机制保证类加载器自下而上的委派,又自上而下的去加载类,这样可以保证每一个类在各个类加载器中都是同一个类。
一个非常明显的目的就是保证java官方的类库
例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。
如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。
五、总结
- java 的类加载,就是获取.class文件的二进制字节码数组并加载到 JVM 的方法区,并在 JVM 的堆区建立一个用来封装 java 类相关的数据和方法的java.lang.Class对象实例。
- java默认有的类加载器有三个,启动类加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用程序类加载器(也叫系统类加载器)(AppClassLoader)。类加载器之间存在父子关系,这种关系不是继承关系,是组合关系。如果parent=null,则它的父级就是启动类加载器。启动类加载器无法被java程序直接引用。