站在虚拟机角度来看,只存在两种类加载器,一种是启动类加载器(Bootstrap ClassLoader),这个类加载器一般由C++实现,是虚拟机的一部分;另外一种是其他所有类加载器,这些类加载器由Java实现,独立存在于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader
自JDK 1.2以来,Java一直保持着三层类加载器,双亲委派的类加载架构。
在Java 8及以前版本绝大多数Java程序都会使用以下三个系统提供的类加载器来进行加载
1)启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载存放在在**
\LIB**目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机所能识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户编写自定义类加载器,如果需要把加载请求委派给引导类加载器去处理,那么直接使用null来替代即可 2)扩展类加载器(Extension Class Loader):这个类加载器是类sum.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载**
\LIB\ext**目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。根据扩展类加载器这个名称可以推断出,这是一种Java系统类库的扩展机制,JDK开发团队允许用户将具有通用性的类库放在ext目录下以扩展JavaSE的功能,在JDK 9之后,这种扩展机制被模块化所带来的天然的扩展能力所取代,由于扩展类加载器是由Java实现的,所以可以直接调用扩展类加载器来加载Class文件 3)应用程序类加载器(Application Class Loader):这个类加载器由sum.misc.Launcher$Application ClassLoader来实现。由于这个加载器是ClassLoader类中getSystemClassLoader()方法的返回值,所以有时候也称为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果程序中没有自定义过类加载器,一般默认就是这个类加载器
JDK 9之前都是由这个三种类加载器互相配合完成加载的,如果用户需要自定义类加载器,可以自己扩展
各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”,,双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,不过这里的类加载器之间的父子关系不是一般的继承(Inheritance)关系,而是通过组合(Composition)关系来复用父加载器的代码
双亲委派机制的工作流程是:如果一个类加载器收到了类加载请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终应该传送给最顶层的启动类加载器去完成,只有父类加载器反馈无法加载(没有搜索到这个类)时才会尝试自己去加载
使用双亲委派模型的优点:
显而易见的是Java中的类随着它的类加载器一起具备了优先级层次关系,例如java.lang.Object,它存放于rt.jar中,无论哪一个类加载器要加载这个类,最终都要委托给启动类加载器,因此Object类在程序任何类加载器环境中都能保证是同一个类, 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
避免了多份同样字节码的加载,内存是宝贵的,没必要保存相同的两份 Class 对象,例如 System.out.println() ,实际我们需要一个 System 的 Class 对象,并且只需要一份,如果不使用委托机制,而是自己加载自己的,那么类 A 打印的时候就会加载一份 System 字节码,类 B 打印的时候又会加载一份 System 字节码。而使用委托机制就可以有效的避免这个问题
双亲委派模型对于保证 Java 程序的稳定运作很重要,但它的实现却非常的简单,实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中。如下所示
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查请求的类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类抛出ClassNotFoundException
// 说明父类无法完成加载请求
}
if (c == null) {
// 在父类无法加载时
// 再调用本身的 findClass方法进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。
而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。
这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写 loadClass,重写了之后就可以破坏委派逻辑了。
第二次破坏指的是 JNDI、JDBC 之类的情况。MySQL 有 MySQL 的 JDBC 实现,Oracle 有 Oracle 的 JDBC 实现,我 Java 不管你内部如何实现的,反正你们这些数据库厂商都得统一按我这个来,这样我们 Java 开发者才能容易的调用数据库操作,所以在 Java 核心包里面定义了这个 SPI。
而核心包里面的类都是由启动类加载器去加载的,但它的手只能摸到
或Xbootclasspath指定的路径中,其他的它鞭长莫及。
\lib 而 JDBC 的实现类在我们用户定义的 classpath 中,只能由应用类加载器去加载,所以启动类加载器只能委托子类来加载数据库厂商们提供的具体实现,这就违反了自下而上的委托机制。
具体解决办法是搞了个线程上下文类加载器,通过
setContextClassLoader()
默认情况就是应用程序类加载器,然后利用Thread.current.currentThread().getContextClassLoader()
获得类加载器来加载。
这次破坏是为了满足热部署的需求,不停机更新这对企业来说至关重要,毕竟停机是大事。
OSGI 就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托,有很多平级之间的类加载器查找,具体就不展开了,有兴趣可以自行研究一下。
在 JDK9 引入模块系统之后,类加载器的实现其实做了一波更新。
像扩展类加载器被重命名为平台类加载器,核心类加载归属了做了一些划分,平台类加载器承担了更多的类加载,上面提到的 -Xbootclasspath、java.ext.dirs 也都无效了,rt.jar 之类的也被移除,被整理存储在 jimage 文件中,通过新的 JRT 文件系统访问。
当收到类加载请求,会先判断该类在具名模块中是否有定义,如果有定义就自己加载了,没的话再委派给父类。
关于 JDK9 相关的知识点就不展开了,有兴趣的自行查阅。