Java 虚拟机设计团队有意把类加载阶段中的"通过一个类的全限定名来获取描述该类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个Java 虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

双亲委派模型

站在 Java 虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是其他所有 的类加载器,这些类加载器都由 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

意义

Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

如何委派

双亲委派并不是 JVM 层面的限制,是 Java 核心代码中的一种实现策略,举个极端点的例子,虽然我们上面提到了双亲委派解决不同类加载器加载同名(全限定名)的不同类的问题,但是 Object 类的唯一性并不能使用双亲委派保证,而是在 defineClass 中以检查的方式保证。如果是自己实现的类加载器,那么需要自己实现这个策略(这也是双亲委派的一种破坏方式,这是为了兼容在双亲委派这个策略使用之前用户实现的类加载器)。

JNDI 破坏双亲委派

JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的 ClassPath 下的 JNDI 服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread 类的 setContext-ClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

JNDI 服务使用这个线程上下文类加载器去加载所需的 SPI 服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为。当 SPI 的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK 6时,JDK提供了 java.util.ServiceLoader 类,以META-INF/services中的配置信息,辅以责任链模式。

JNDI 具体的加载逻辑可见:以JDBC为例谈双亲委派模型的破坏 - 掘金 (juejin.cn)

OSGI 破坏双亲委派

OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

1.  将以java.*开头的类,委派给父类加载器加载
2. 否则,将委派列表名单内的类,委派给父类加载器加载
3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载
4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
7. 否则,类查找失败。

Web 服务器的类加载

Web 服务器(例如 tomcat )上部署 Java 服务程序时需要处理 Java 类库的隔离和共享的问题,比如 tomcat 上同时部署的两个 Java 服务程序依赖了不同版本的某个类库,那么这两个类库需要隔离,而二者共同使用的类库,比如同一个版本的 spring 则应该共享。所以一般的 Web 服务器都会提供不同的 ClassPath 供程序员存放不同的类库,每一个 ClassPath 中都会有一个相应的类加载器去加载路径中的类库(比如 tomcat 就提供了 /common, /server, /shared, /WebApp/WEB-INF 4个 ClassPath)。

他们的双亲委派模型如下

依据双亲委派,Catalina 类加载器和 Shared 类加载器都可以使用 Common 类加载器加载其 ClassPath 中的类,但 Catalina 类加载器和 Shared 类加载器能够加载的类是彼此隔离的,WebApp 可以使用 Shared 类加载器加载相应路径下的类,但是各个不同的 WebApp 的类加载路径是隔离的。Jsp 类加载器加载的类的路径仅是某个 Jsp 文件编译出来的 .class 文件,如果检测到 Jsp 文件被修改,那么当前的 Jsp 类加载器将被替换为一个新的 Jsp 类加载器实例,加载 Jsp 编译后的新 .class,实现 HotSwap(热切换)。