【从零开始学习JVM | 第四篇】类加载器的分类以及双亲委派机制

前言:

在Java编程中,类加载器(Class Loader)扮演着重要的角色。类加载器负责加载Java字节码并将其转换为可执行对象,使得我们能够在应用程序中使用各种类和资源。Java类加载器的设计和实现旨在支持动态扩展和模块化编程,为Java语言提供了很大的灵活性和可维护性。

类加载器的分类是理解Java类加载机制的关键部分。不同类型的类加载器负责加载不同类型的类,它们有着不同的加载策略和优先级。在本文中,我们将深入探讨类加载器的分类,并详细介绍每一种类加载器的特点和使用场景。

目录

前言:

类加载器: 

什么是类加载器: 

类加载器的应用场景:

类加载器的分类(JDK8以及之前版本): 

双亲委派机制:

什么是双亲委派机制:

双亲委派机制源码解读:

双亲委派机制的作用:

打破双亲委派机制:

总结:


类加载器: 

什么是类加载器: 

类加载器(ClassLoader)是JAVA虚拟机提供给应用程序去实现类和接口字节码数据的技术。

类加载器只参与加载过程中字节码获取并加载到内存中这一部分

类加载器的应用场景:

  1. 动态加载:类加载器允许在运行时动态地加载新的类和资源。这对于需要根据特定条件或用户需求加载不同类的应用程序非常有用。例如,插件系统和模块化开发中经常会使用类加载器动态加载和卸载模块,实现灵活的扩展和功能定制。

  2. 热部署:类加载器可以实现热部署,即在应用程序运行过程中替换和更新已加载的类。这使得我们可以在不停止应用程序的情况下对代码进行更新和修复。热部署在开发和调试阶段非常有用,能够提高开发效率和调试体验。

  3. 版本隔离:通过使用不同的类加载器加载不同版本的类,我们可以实现类的版本隔离。这在应用程序升级和依赖管理中十分重要,避免了不同版本的类之间的冲突和兼容性问题。

  4. 安全控制:类加载器可以应用安全策略,限制或控制特定代码和资源的访问权限。通过自定义类加载器,我们可以实现自定义的安全策略,对加载的类进行权限控制和验证。

  5. 字节码增强和动态代理:类加载器可以用于实现字节码增强和动态代理。通过自定义类加载器,我们可以在加载类的过程中修改字节码,添加额外的逻辑或功能。这对于AOP(面向切面编程)和代理模式等技术有着重要的应用。

而且类加载器的双亲委派机制也是面试中必问的一块内容,只要面试官问JVM相关的内容,那么双亲委派机制就一定是必问项。因此我们要学好类的加载器相关内容。

在了解了什么是类加载器以及其应用场景之后,我们来正式学习类加载器。


类加载器的分类(JDK8以及之前版本): 

  1. 启动类加载器(Bootstrap ClassLoader):它是JVM的一部分,负责加载Java核心类库,如java.lang包中的类。它是最顶层的类加载器,通常使用C++实现,无法在Java代码中直接获取到。(通用且重要)

  2. 扩展类加载器(Extension ClassLoader):它负责加载Java的扩展库,通常位于JRE的lib/ext目录下。它是由Java编写的,是由启动类加载器加载的。(通用但是不重要)

  3. 应用程序类加载器(Application ClassLoader):也称为系统类加载器(System ClassLoader),它负责加载用户类路径(Classpath)上指定的类库。它是开发者最常使用的类加载器,也是默认的类加载器。

除了上述三种常见的类加载器之外,还可以通过继承 java.lang.ClassLoader 类来自定义类加载器。自定义类加载器可以灵活加载类,实现各种特定需求,比如从网络下载类文件、解密等。

总结起来,常见的类加载器可以分为:启动类加载器、扩展类加载器、应用程序类加载器。开发者也可以根据需要自定义类加载器。

而在实际JAVA代码中,我们可能会遇到一个JAR包同时存在于多个类加载器加载范围的情况,此时我们就需要双亲委派机制来解决这个问题:

双亲委派机制:

什么是双亲委派机制:

双亲委派机制(Parent Delegation Model)是Java类加载器的一种工作方式,用于保证类的加载安全性一致性

根据双亲委派机制,当一个类加载器需要加载某个类时,它首先会委派给它的父类加载器去尝试加载。如果父类加载器能够成功加载该类,那么就返回该类的Class对象。如果父类加载器无法加载该类,子类加载器才会尝试自己去加载。

 

简单来讲:双亲委派机制的核心是解决一个类到底由谁进行加载的问题。而其过程就是向上查找是否加载过和向下尝试加载

注意点:不能认为一个加载器与其父类加载器的关系是继承,虽然有“父”。而本质是在加载器内部创建一个ClassLoader来存储其父类加载器。 

需要注意的是如果我们尝试获取扩展类加载器的parent对象,得到的结果是null。并不是说其父类不是启动类加载器,只是因为启动类加载器属于JVM的一部分,使用C++实现,无法被获取到! 

双亲委派机制源码解读:

整个双亲委派机制都是在Classload中进行的,因此我们主要看这部分源码:

 尝试加载一个类的时候,我们会调用loadClass方法,该方法的第一个参数为加载的类名第二个参数为是否对类进行解析

进入loadClass方法

 其实这段代码还是比较容易看懂的,整体的逻辑为:

  1. 使用findLoadedClass寻找目标类是否被加载
  2. 如果目标类没有被加载(c==null)那么就尝试寻找当前加载器的父类加载器,如果有父类加载器(parent!=null),就把当前类交给父类加载器执行loadClass方法。如果没有父类加载器,就让启动类加载器(BootstrapClassLoad)进行查找并加载
  3. 如果一直到顶层加载器,仍然无法加载目标类,那么我们就交由当前加载器进行加载(c=findClass(name)),并且记录一下时间等各种信息,然后return 0;
  4. 如果目标类已经被加载,直接return 0;

最后我们看一下 c=findClass(name)的findClass源码:

双亲委派机制的作用:

  1. 避免重复加载:通过使用双亲委派机制,每个类加载器在尝试加载某个类之前,都会先委托给它的父类加载器。这样可以避免同一个类被多个不同的类加载器加载,保证类的一致性,避免重复加载带来的冲突和内存浪费。

  2. 安全性保证:核心类库(如Java的核心类库)由启动类加载器负责加载,用户自定义的类则由应用程序类加载器加载。这样可以确保核心类库的安全性,防止用户自定义的类篡改核心类库的行为。

  3. 类的隔离性:不同的类加载器加载的类位于不同的命名空间中,彼此之间互相隔离。即使两个类的全限定名相同,但由不同的类加载器加载的类在JVM中也被视为不同的类。这种隔离性可以有效避免类的冲突,使得每个类加载器都可以独立加载和管理类。

  4. 扩展性:通过自定义类加载器,可以扩展Java的类加载机制,实现特定的加载需求。开发者可以自定义类加载器来实现类似热部署、动态加载等功能。自定义类加载器可以继承父类加载器的特性,并根据业务需求进行扩展。

总的来说,双亲委派机制可以保证类的一致性、安全性和隔离性,避免重复加载,同时也提供了灵活的扩展性,使得类加载器可以根据特定需求进行定制。

而虽然双亲委派机制为JAVA类的加载提供了很好的安全性便捷性。但是有的时候我们不得不打破双亲委派机制,例如:

一个Tomcat容器中可以运行多个WEB应用,而如果这两个应用中出现了同名的A类,那么Tomcat就要保证这两个A类都被加载并且是各自不同的类。如果不打破双亲委派机制,那么WEB1中的A类记载后,WEB2中自己的A类就不会加载成功了,按照双亲委派机制来讲,此时会直接返回WEB1中的A类。此时我们就需要打破双亲委派机制。

打破双亲委派机制:

1.自定义类加载器并且重写Classload(Tomcat使用策略):

通过上文我们对源码单独阅读,相信大家已经理解了双亲委派机制的基本流程。而我们如果想要打破双亲委派机制,重写一下Classload方法就好,具体地讲,是重写以下代码块:

代码示例:

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                // 检查类是否在系统类加载器中已经加载
                c = findClass(name);
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 在这里实现自定义的类加载逻辑
        // 可以从其他位置加载类的字节码,并使用 defineClass() 方法定义类
    }
}

但需要注意的是,在这段代码的逻辑中,虽然我们没有给自定义类加载任何父类加载器,但是他也会有一个默认的父类加载器 应用程序类加载器,只不过我们重写loadClass的时候并没有用到父类加载器而已。

如果我们只是想自定义一个加载器,自主加载一些类。此时就不应该打破双亲委派机制,而是选择在FindClass中进行重写

2.线程上下文类加载器(JDBC使用策略):

JDBC在尝试连接数据库的时候会使用到一个叫做DriveManager的包来管理各种数据库驱动和加载相关驱动:

String url = "jdbc:mysql://localhost:3306/your_database_name";
String username = "your_username";
String password = "your_password";
Connection connection = DriverManager.getConnection(url, username, password);

 DriveManager位于rt.jar中,由启动类加载器进行加载。

而这个包又要去加载各种数据库驱动类。而这种第三方的包又要在应用程序加载类中进行加载。那么就出现了一个问题

也就是说启动类加载器加载完DriveManager之后,对于其需要加载的各种数据库驱动,启动类加载器是无法进行加载的,他只能交给应用程序类加载器进行加载。这不就打破了双亲委派机制的从下向上委托原则

我们一步一步来解析,首先先来介绍一下SPI机制:

        SPI(Service Provider Interface)是Java提供的一种服务提供发现机制。它允许开发人员定义服务接口,并允许第三方厂商通过在应用程序的类路径下提供实现来扩展应用程序的功能。

SPI机制的工作原理如下:首先,开发人员定义一个服务接口,以及对该接口提供服务实现的一个或多个类。然后,在应用程序的类路径中创建一个配置文件,该文件的名称必须是"META-INF/services/接口全限定名",其中,以接口全限定名作为文件名,其内容则是服务接口实现类的全限定名。

当应用程序初始化时,Java运行时会利用Java的反射机制从类路径下的配置文件中读取并加载服务接口的实现类。这样,应用程序就能够获取到实现类的实例,并使用其提供的功能。

而我们的DriveManager就是通过SPI机制快速找到JaR包要加载的驱动的。

而基于SPI机制,DriveManager被加载的整体过程为:

在了解了基于基于SPI机制下DriveManager被加载的整体过程后,我们再来想一想:SPI机制是如何打破双亲委托机制下的从下委托的呢?

在SPI机制中,通常使用线程上下文类加载器(Thread Context Class Loader)来加载具体的实现类。线程上下文类加载器是在多线程环境中引入的概念,用于指定每个线程的类加载器。线程上下文类加载器通常通过Thread.currentThread().setContextClassLoader()方法进行设置。

在SPI机制中,通过线程上下文类加载器,可以解决在双亲委托模型下从底层向上委托的问题。具体来说,当SPI实现框架的代码位于一个类库中,而由应用程序自定义的SPI实现类位于应用程序的类路径下时,由于双亲委托模型的限制,无法直接由应用程序加载SPI实现类。此时可以通过在应用程序中使用线程上下文类加载器来加载SPI实现类,即将线程上下文类加载器设置为应用程序的类加载器。这样,SPI实现框架就可以通过线程上下文类加载器加载应用程序中的SPI实现类,从而打破了双亲委托模型的限制。

需要注意的是,SPI机制依赖于线程上下文类加载器的正确设置,因此在使用SPI机制时,需要确保正确设置线程上下文类加载器,以保证SPI实现框架能够正确加载应用程序中的SPI实现类。

简单来讲:SPI有上下文类加载器,他可以提前保存好一个应用类程序加载器。然后当我们使用启动类加载器加载DriveManager,而DriveManager需要加载数据库驱动的时候,DriveManager就会调用上下文类加载器,使得当前加载器从启动类加载变为应用类加载器

但其实对于上下文加载器打破双亲委派机制这种方式呢,普遍还是存在争议的。

  • 有人认为他确实打破双亲委派机制:因为 DriveManager 由启动类加载器加载,却在记载过程中需要委派程序类加载器进行记载,打破了双亲委派机制的委派是从上到下的规则。
  • 有人认为他没有打破双亲委派机制:因为在整个加载类的过程中,DriveManager在java核心包rt.jar中,因此被启动类加载器加载;jar包中的数据库驱动属于第三方包,因此被从应用程序类加载器加载。不管是DriveManager类还是数据库驱动类的加载,都没有重写loadClass方法,只要你使用的是原生的loadClass,你就仍然遵循双亲委派机制

3.OSGI框架的类加载器:

历史上OSGI模块化框架打破了双亲委派机制,它存在同级之间的类记载器的委托加载。

OSGi(开放服务网关)是一个用于构建模块化、动态、可扩展的Java应用程序的规范和框架。

模块化是指将应用程序拆分为多个独立的模块(也称为bundle),每个模块包含自己的代码和资源。这种模块化的设计使得开发人员可以更加灵活地管理和维护应用程序,提高了可重用性和可维护性。

最早的时候JAVA是没有模块化的思想的,所有的jar包都在rt.jar中进行管理,而OSGi就提供了一种方式将功能相近的jar包放入到一个jar包进行统一管理。

在OSGi框架中,每个模块被称为一个bundle(捆绑包),bundle可以包含自己的类和资源。OSGi使用了自己的类加载器实现,称为BundleClassLoader。

BundleClassLoader是OSGi框架中的核心类加载器,它在加载类时打破了双亲委派机制。它首先尝试自己加载类,如果找不到所需的类,则会委托给父类加载器。这种机制与标准的双亲委派机制不同,因为BundleClassLoader首先尝试自己加载,并不一定按照父优先的原则。

总结:

类加载器和双亲委派机制是Java中关键的概念。通过类加载器,Java程序能够动态加载类,实现代码的灵活性和可扩展性。双亲委派机制保证了类的唯一性和一致性,避免了重复加载和冲突。了解类加载器和双亲委派机制对于解决类路径冲突、实现模块化开发和保证安全性至关重要。它们对于掌握Java动态加载能力和模块化开发具有重要意义。深入理解其原理和机制,能更好地利用Java的灵活性和可扩展性,开发出优秀的应用程序。在某些情况下,可能需要定制和扩展类加载器行为,进一步发挥其作用。总之,类加载器和双亲委派机制是Java虚拟机的核心组成部分,对于理解和掌握Java的动态加载能力和模块化开发至关重要。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

猜你喜欢

转载自blog.csdn.net/fckbb/article/details/134842083