JVM —— 类加载器的分类,双亲委派机制

简介: 类加载器的分类,双亲委派机制:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器;JDK8及之前的版本,JDK9之后的版本;什么是双亲委派模型,双亲委派模型的作用,如何打破双亲委派机制

文章目录

一、类加载器的分类【理解】

  • 1.1 概述

  • 1.2 JDK8及之前的版本

    • 1.2.1 启动类加载器

    • 1.2.2 扩展类加载器和应用程序类加载器

      • 扩展类加载器
      • 应用程序类加载器
  • 1.3 JDK9之后的类加载器

  • 1.4 ClassLoader 中的两个方法【应用】

二、双亲委派模型【理解】

  • 2.1 什么是双亲委派机制

    • 面试题:类的双亲委派机制是什么
  • 2.2 双亲委派机制源码解读

  • 2.3 JVM为什么采用双亲委派机制

  • 2.4 双亲委派机制的作用

  • 2.5 打破双亲委派机制

    • 2.5.1 自定义类加载器(Tomcat)
    • 2.5.2 线程上下文类加载器(如JDBC)
    • 2.5.3 OSG框架的类加载器

三、小节

一、类加载器的分类【理解】

还记得类加载器的定义、作用、类加载的完整过程吗?如果忘记可以到这里重新温习: 类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类 ,此处重点讲述类加载器的分类。

1.1 概述

类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。

  • 虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言与虚拟机底层语言一致,比如Hotspot使用C++。主要目的是保证Java程序运行的基础类被正确地加载,比如java.lang.String,Java虚拟机需要确保其可靠性。

  • JDK中默认提供或者自定义(重点关注):JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求使用Java语言定制。所有Java中实现的类加载器都需要继承ClassLoader这个抽象类。

1-1.png

1-2.png

类加载器的设计,JDK8和8之后的版本差别较大(JDK9之后,出现了模块化设计)。

1.2 JDK8及之前的版本

首先来看JDK8及之前的版本,JDK8及之前的版本中默认的类加载器有如下几种

  • 启动类加载器(Bootstrap ClassLoader、C++实现):加载JAVA_HOME/jre/lib目录下的库,加载核心类,String类。它是JVM的一部分,负责加载Java核心类库,如java.lang包中的类。它是最顶层的类加载器,通常使用C++实现,无法在Java代码中直接获取到。通常表示为null ,并且没有父null(通用且重要)

  • 扩展类加载器(Extension ClassLoader、Java实现):主要加载JAVA_HOME/jre/lib/ext目录中的类。加载扩展类,拓展Java中比较通用的类,只是通用,不是特别重要,最重要的在启动类加载器加载了。通常位于JRE的lib/ext目录下

  • 应用程序类加载器(Application ClassLoader、Java实现):也称为系统类加载器(System ClassLoader),加载classPath下的类。加载应用classpath中的类,包括我们自己写的类,还有第三方Jar包的类

  • 自定义类加载器(Java实现):可以通过继承 java.lang.ClassLoader 类来自定义类加载器,需要重写findClass方法,实现自定义类加载规则。自定义类加载器可以灵活加载类,实现各种特定需求,比如从网络下载类文件、解密等。

    JDK9及之后扩展类加载器(Extension ClassLoader)变成了平台类加载器(Platform ClassLoader)

1-3.png

1-4.png

代码演示

public class ClassLoaderClassDemo1 {
   
    public static void main(String[] args) {
   
        //获取应用程序类加载器/系统类加载器                sun.misc.Launcher$AppClassLoader@18b4aac2
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

        //获取应用程序类加载器的父加载器 --- 扩展类加载器    sun.misc.Launcher$ExtClassLoader@6a6824be
        ClassLoader classLoader1 = systemClassLoader.getParent();

        //获取扩展类加载器的父加载器 --- 启动类加载器       null
        ClassLoader classLoader2 = classLoader1.getParent();

        System.out.println("应用程序类加载器" + systemClassLoader);
        System.out.println("扩展类加载器" + classLoader1);
        System.out.println("启动类加载器" + classLoader2);
    }
}

补充:Arthas中类加载器相关的功能

Arthas是程序员开发运维必不可少的一个工具,还记得如何使用吗?忘记的话,可以参考 Java字节码文件、组成、详解、分析;jclasslib插件、阿里arthas工具;Java注解

类加载器的详细信息可以通过classloader命令查看:

classloader 查看classloader的继承树,urls,类加载信息,使用classloader去getResource

1-5.png

1-6.png

第1列为类加载名称,第2列为当前类加载器在内存中实例个数,第3列为当前类加载器加载了多少个类。

  • BootstrapClassLoader是启动类加载器,numberOfInstances是类加载器的数量只有1个,loadedCountTotal是加载器所加载的类的数量为1861个
  • ExtClassLoader是扩展类加载器
  • AppClassLoader是应用程序类加载器
  • DelegatingClassLoader是用来提升反射效率的类加载器

1.2.1 启动类加载器

  • 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器,Java程序员无法修改或者扩展源代码,所以只关注这个加载器的作用。
  • 作用:默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等,给java程序提供了一个基础的运行环境

1-7.png

在IDEA项目右侧External Libraries中也能找到对应jar包,这就是启动类加载器所加载的。

1-8.png

/**
 * 启动类加载器案例
 */
public class BootstrapClassLoaderDemo {
   
    public static void main(String[] args) throws IOException {
   
        //通过String类获取到它的类加载器。String.class 取到当前堆上的class对象
        ClassLoader classLoader = String.class.getClassLoader();
        System.out.println(classLoader);   //输出null

        //让程序不再退出
        System.in.read();
    }
}

1-9.png

这段代码通过String类获取到它的类加载器并且打印,本来以为是Bootstrap ClassLoader,结果是null。这是因为启动类加载器在JDK8中是由C++语言来编写的,在Java代码中去获取既不适合也不安全,所以才返回null(String类确实是由启动类加载器加载的,但是启动类加载器由虚拟机底层实现、没有存在Java代码中,无法通过Java代码获取底层的虚拟机启动类加载器)

在Arthas中可以通过sc -d 类名的方式查看加载这个类的类加载器详细的信息,如

1-10.png

通过上图可以看到,java.lang.String类的类加载器是空的,Hash值也是null。所以只要看到class-loader为null,就知道这是启动类加载器

通过启动类加载器去加载用户jar包:

如果用户想扩展一些比较基础的jar包,让启动类加载器加载,有两种途径:

  • 打包成jar包,放入jre/lib下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容,因为即使放进去由于文件名不匹配的问题也不会正常地被加载(在加载jar包的时候,会对名称进行校验,名称必须符合JVM内部的一些规范)。
  • 使用参数进行扩展。推荐,使用-Xbootclasspath/a:jar包目录/jar包名进行扩展,参数中的/a代表新增。

下面展示方式二实现流程:

先创建第一个项目,mvn package打包成jar包,把jar包重命名放到D:/jvm/jar目录下,即D:/jvm/jar/classloader-test.jar;

1-11.png

再创建第二个项目,在第二个项目的IDEA配置中添加虚拟机参数,就可以加载D:/jvm/jar/classloader-test.jar这个jar包了

1-12.png

希望启动类加载帮我们加载A类,在另一个项目中获取A类并初始化:使用Class.forName获取Jar包的类,可以正常执行初始化,说明自己拓展的Jar包被加载了

1-13.png

应用场景:在企业中开发一些偏底层的基础类,所有用到jdk的项目都需要使用这些基础类,此时就通过启动类加载器去加载用户jar包

1.2.2 扩展类加载器和应用程序类加载器

  • 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器。
  • 它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader,具备通过目录或者指定jar包将字节码文件加载到内存中的能力。

继承关系图如上:

1-14.png

1-15.png

  • ClassLoader类:定义了具体的行为模式,简单来说就是先从本地或者网络获得字节码信息,然后调用虚拟机底层的方法创建方法区和堆上的对象。这样的好处就是让子类只需要去实现如何获取字节码信息这部分代码。

  • SecureClassLoader:提供了证书机制,提升了安全性。

  • URLClassLoader:提供了根据URL获取目录下或者指定jar包进行加载,获取字节码的数据的能力。

扩展类加载器和应用类加载器继承自URLClassLoader,获得了上述的三种能力。

扩展类加载器

扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器。默认加载Java安装目录/jre/lib/ext下的类文件。

1-16.png

如下代码会打印ScriptEnvironment类的类加载器。ScriptEnvironment是nashorn框架中用来运行javascript语言代码的环境类,他位于nashorn.jar包中被扩展类加载器加载。这些类我们很少用,所以被放到了扩展类加载器中。

/**
 * 扩展类加载器
 */
public class ExtClassLoaderDemo {
   
    public static void main(String[] args) throws IOException {
   
        ClassLoader classLoader = ScriptEnvironment.class.getClassLoader();
        System.out.println(classLoader);
    }
}

1-17.png

通过扩展类加载器去加载用户jar包

  • 放入/jre/lib/ext下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容。
  • 使用参数进行扩展使用参数进行扩展。推荐,使用-Djava.ext.dirs=jar包目录进行扩展,这种方式会覆盖掉原始目录(jre-xx/lib/ext),可以追加上原始目录,并使用 ;(windows系统所用符号) :(macos/linux) 进行分隔

1-18.png

确保自己写的类由扩展类加载器加载(上述A类),ScriptEnvironment仍由扩展类加载器加载、不受影响

1-19.png

使用引号将整个地址包裹起来,这样路径中即便是有空格也不需要当做特殊字符额外处理。路径中要包含原来ext文件夹,同时在最后加上扩展的路径。

1-20.png

应用程序类加载器

应用程序类加载器会加载classpath下的类文件,默认加载的是项目中的类以及通过maven引入的第三方jar包中的类。

如下案例中,打印出Student(自己写的)和FileUtils(引入的)的类加载器:

/**
 * 应用程序类加载器案例
 */
public class AppClassLoaderDemo {
   
    public static void main(String[] args) throws IOException, InterruptedException {
   
        //当前项目中创建的Student类
        Student student = new Student();
        ClassLoader classLoader = Student.class.getClassLoader();
        System.out.println(classLoader);

        //maven依赖中包含的类
        ClassLoader classLoader1 = FileUtils.class.getClassLoader();
        System.out.println(classLoader1);

        Thread.sleep(1000);
        //由于使用Arthas监控该程序,故加上SYstem.in.read()让主方法不退出
        System.in.read();

    }
}

输出结果如下,这两个类均由应用程序类加载器加载:

1-21.png

Arthas中类加载器相关功能

类加载器的加载路径可以通过classloader –c hash值查看:

1-22.png

查看应用程序类加载器所加载的jar包

1-23.png

1.3 JDK9之后的类加载器

1-24.png

由于JDK9引入了module的概念,类加载器在设计上发生了很多变化

1-25.png

1)启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。

启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一

1-26.png

2)扩展类加载器被替换成了平台类加载器(Platform Class Loader)。平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinCLassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑

1-27.png

1.4 ClassLoader 中的两个方法【应用】

  • 方法介绍
方法名 说明
public static ClassLoader getSystemClassLoader() 获取系统类加载器
public InputStream getResourceAsStream(String name) 加载某一个资源文件
  • 示例代码
public class ClassLoaderDemo2 {
   
    public static void main(String[] args) throws IOException {
   
        //static ClassLoader getSystemClassLoader() 获取系统类加载器
        //InputStream getResourceAsStream(String name)  加载某一个资源文件

        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

        //利用加载器去加载一个指定的文件
        //参数:文件的路径(放在src的根目录下,默认去那里加载)
        //返回值:字节流。
        InputStream is = systemClassLoader.getResourceAsStream("prop.properties");

        Properties prop = new Properties();
        prop.load(is);

        System.out.println(prop);

        is.close();
    }
}

二、双亲委派模型【理解】

上文已经介绍过类加载器分类,在实际Java代码中,我们可能会遇到一个JAR包同时存在于多个类加载器加载范围的情况,此时我们就需要双亲委派机制来解决这个问题。

2-1.png

2.1 什么是双亲委派机制

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

根据双亲委派机制,加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类

简单来讲:双亲委派机制的核心是解决一个类到底由谁进行加载的问题。当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,在自顶向下进行加载

具体介绍:如果一个类加载器收到了类加载请求、需要加载某个类时,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

2-2.png

2-3.png

2-4.png

2-5.png

双亲委派机制-问题

  • 重复的类:如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载? ——启动类加载器加载,根据双亲委派机制,它的优先级是最高的
  • String类能覆盖吗:在自己的项目中去创建一个java.lang.String类,会被加载吗? ——不能,会返回启动类加载器加载在rt.jar包中的String类

在Java中如何使用代码的方式去主动加载一个类呢?

  • 方法1:使用Class.forName方法,使用当前类的类加载器去加载指定的类
  • 方法2:获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载
public class String {
   
   static {
   
       System.out.println("自己写的String类被加载了...");
   }
}
public class Demo4 {
   
    public static void main(String[] args) throws ClassNotFoundException {
   
        //获取main方法所在类的类加载器,应用程序类加载器
        ClassLoader classLoader = Demo4.class.getClassLoader();
        System.out.println(classLoader);    //sun.misc.Launcher$AppClassLoader@18b4aac2 启动类加载器

        //使用应用程序类加载器加载 手写的com.lang.String
        Class<?> stringClazz = classLoader.loadClass("java.lang.String");
        System.out.println(stringClazz.getClassLoader());  //null  扩展类加载器
    }
}

2-6.png

2-7.png

2-8.png

面试题:类的双亲委派机制是什么

1)当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。

2)应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。

3)双亲委派机制的好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。

2.2 双亲委派机制源码解读

每个Java实现的类加载器中保存了一个成员变量叫“父”(Parent)类加载器,可以理解为它的上级,并不是继承关系。本质是在加载器内部创建一个ClassLoader来存储其父类加载器。

2-9.png

  • 应用程序类加载器的parent父类加载器是扩展类加载器,而扩展类加载器的parent是空,但是在代码逻辑上,扩展类加载器依然会把启动类加载器当成父类加载器处理
  • 启动类加载器使用C++编写,没用父类加载器

2-10.png

Arthas中类加载器相关的功能:类加载器的父子关系可以通过classloader -t查看

2-11.png

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

public Class<?> loadClass(String name) throws ClassNotFoundException {
   
    return loadClass(name, false);
}

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

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
   
    synchronized (getClassLoadingLock(name)) {
   
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
   
            long t0 = System.nanoTime();
            try {
   
                if (parent != null) {
   
                    c = parent.loadClass(name, false);
                } else {
   
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
   
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
   
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
   
            resolveClass(c);
        }
        return c;
    }
}

这段代码整体的逻辑为:

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

2.3 JVM为什么采用双亲委派机制

(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。

(2)为了安全,保证类库API不会被修改

package java.lang;
public class String {
   
    public static void main(String[] args) {
   
        System.out.println("demo info");
    }
}

由于是双亲委派的机制,java.lang.String的在启动类加载器得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。

此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法

2-12.png

2.4 双亲委派机制的作用

  • 避免重复加载:通过使用双亲委派机制,每个类加载器在尝试加载某个类之前,都会先委托给它的父类加载器。这样可以避免同一个类被多个不同的类加载器加载,保证类的一致性,避免重复加载带来的冲突和内存浪费。
  • 保证类加载的安全性:核心类库(如Java的核心类库)由启动类加载器负责加载,用户自定义的类则由应用程序类加载器加载。这样可以确保核心类库的安全性,防止用户自定义的类篡改核心类库的行为,比如java.lang.String。
  • 类的隔离性:不同的类加载器加载的类位于不同的命名空间中,彼此之间互相隔离。即使两个类的全限定名相同,但由不同的类加载器加载的类在JVM中也被视为不同的类。这种隔离性可以有效避免类的冲突,使得每个类加载器都可以独立加载和管理类。
  • 扩展性:通过自定义类加载器,可以扩展Java的类加载机制,实现特定的加载需求。开发者可以自定义类加载器来实现类似热部署、动态加载等功能。自定义类加载器可以继承父类加载器的特性,并根据业务需求进行扩展。

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

而虽然双亲委派机制为JAVA类的加载提供了很好的安全性和便捷性。但是有的时候我们不得不打破双亲委派机制,例如:一个Tomcat容器中可以运行多个WEB应用,而如果这两个应用中出现了同名的A类,那么Tomcat就要保证这两个A类都被加载并且是各自不同的类。如果不打破双亲委派机制,那么WEB1中的A类记载后,WEB2中自己的A类就不会加载成功了,按照双亲委派机制来讲,此时会直接返回WEB1中的A类。此时我们就需要打破双亲委派机制。

2.5 打破双亲委派机制

打破双亲委派机制的三种方式:

  • 自定义类加载器
    • 自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除
    • Tomcat通过这种方式实现应用之间类隔离
  • 线程上下文加载器
    • 利用上下文类加载器加载类,比如JDBC和JNDI等
  • Osgi框架的类加载器(了解即可)
    • 历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载

2-13.png

2.5.1 自定义类加载器(Tomcat)

2-14.png

2-15.png

2-16.png

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

2-17.png

代码示例:

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的时候并没有用到父类加载器而已。

2-18.png

问题:两个自定义类加载器加载相同限定名的类,不会冲突吗

  • 不会冲突。在同一个Java虚拟机中,只有相同类加载器+相同的类限定名 才会被认为是同一个类
  • 在Arthas中使用sc -d 类名的方式查看具体的情况,sc -d com.xxx.A

2-19.png

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

2-20.png

2.5.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);

2-21.png

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

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

2-22.png

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

DriverManager怎么知道jar包要加载的驱动在哪儿? ——用到JDK中的SPI机制。

  • SPI(Service Provider Interface)是JDK内置、Java提供的一种服务提供发现机制。它允许开发人员定义服务接口,并允许第三方厂商通过在应用程序的类路径下提供实现来扩展应用程序的功能。被大量运用在一些框架中,如阿里的DUbbo框架

  • SPI机制的工作原理如下:首先,开发人员定义一个服务接口,以及对该接口提供服务实现的一个或多个类。然后在应用程序的类路径中创建一个配置文件,该文件的名称必须是"META-INF/services/接口全限定名",其中,以接口全限定名作为文件名、其内容则是服务接口实现类的全限定名。当应用程序初始化时,Java运行时会利用Java的反射机制从类路径下的配置文件中读取并加载服务接口的实现类。这样,应用程序就能够获取到实现类的实例,并使用其提供的功能。

2-23.png

2-24.png

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

public static <S> ServiceLoader<S> load(Class<S> service) {
   
    ClassLoader c1 = Thread.currentThread().setContextClassLoader();
    return ServiceLoader.load(service, c1);
}

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

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

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

2-25.png

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

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

2-26.png

2.5.3 OSG框架的类加载器

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

  • OSGi(开放服务网关)是一个用于构建模块化、动态、可扩展的Java应用程序的规范和框架。
  • 模块化是指将应用程序拆分为多个独立的模块(也称为bundle),每个模块包含自己的代码和资源。这种模块化的设计使得开发人员可以更加灵活地管理和维护应用程序,提高了可重用性和可维护性。
  • 最早的时候JAVA是没有模块化的思想的,所有的jar包都在rt.jar中进行管理,而OSGi就提供了一种方式将功能相近的jar包放入到一个jar包进行统一管理。

2-27.png

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

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

热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中

2-28.png

注意事项:

  • 程序重启之后,字节码文件会恢复,除非将class文件放入jar包中进行更新
  • 使用retransform不能添加方法或者字段,也不能更新正在执行中的方法

三、小节

(1)什么是类加载器

JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

(2)类加载器的作用是什么

类加载器(ClassLoader)负责在类加载器过程中的字节码获取并加载到内存这一部分。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据

(3)类加载器有哪些/有几种常见的类加载器

  • 启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库,加载核心类

  • 扩展类加载器(Extension ClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类,加载扩展类

  • 应用类加载器(Application ClassLoader):用于加载classPath下的类

  • 自定义类加载器(Customize ClassLoader):自定义类继承ClassLoader,重写findClass方法,实现自定义类加载规则。

    JDK9及之后扩展类加载器(Extension ClassLoader)变成了平台类加载器(Platform ClassLoader)

3-1.png

(4)什么是双亲委派机制

每个Java实现的类加载器中保留了一个成员变量叫“父”(Parent)类加载器。

  • 加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
  • 自底向上查找是否加载过,再由顶向下进行加载。避免了核心类被应用程序重写并覆盖的问题,提升了安全性

3-2.png

(5)JVM为什么采用双亲委派机制

3-3.png

  • 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
  • 为了安全,保证类库API不会被修改

(6)怎么打破双亲委派机制

  • 重写loadClass方法,不再实现双亲委派机制
  • JNDI、JDBC、JCE、JAXB和JBI等框架使用了SPI机制+线程上下文类加载器
  • OSGi实现了一整套类加载机制,允许同级类加载器之间互相调用

参考 黑马程序员相关视频及笔记,大部分内容来源于黑马程序员的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),加上自己部分思考

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

相关文章
|
2月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
1月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
42 3
|
1月前
|
前端开发 Java 应用服务中间件
JVM进阶调优系列(1)类加载器原理一文讲透
本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
|
3月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
58 0
|
3月前
|
开发者 C# Windows
WPF布局大揭秘:掌握布局技巧,轻松创建响应式用户界面,让你的应用程序更上一层楼!
【8月更文挑战第31天】在现代软件开发中,响应式用户界面至关重要。WPF(Windows Presentation Foundation)作为.NET框架的一部分,提供了丰富的布局控件和机制,便于创建可自动调整的UI。本文介绍WPF布局的基础概念与实现方法,包括`StackPanel`、`DockPanel`、`Grid`等控件的使用,并通过示例代码展示如何构建响应式布局。了解这些技巧有助于开发者优化用户体验,适应不同设备和屏幕尺寸。
86 0
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
7天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
5天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
62 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!