【Jvm】类加载机制(Class Loading Mechanism)原理及应用场景

文章目录

  • Jvm基本组成
  • 一.什么是JVM类的加载
  • 二.类的生命周期
    • 阶段1:加载
    • 阶段2:验证
    • 阶段3:准备
    • 阶段4:解析
    • 阶段5:初始化
  • 三.类初始化时机
  • 四.类加载器
    • 1.引导类加载器(Bootstrap Class Loader)
    • 2.拓展类加载器(Extension Class Loader)
    • 3.应用程序类加载器(System Class Loader)
    • 4.自定义类加载器(Custom Class Loader)
    • 5.类加载器的关系
    • 6.JVM类加载策略
  • 五.双亲委派机制
    • 1.什么是双亲委派机制
    • 2.浅析ClassLoader类
    • 3.双亲委派模式具体实现
      • 3.1.loadClass(String name)
      • 3.2.findClass(String name)-(重点)
      • 3.3.defineClass(byte[] b, int off, int len)
      • 3.4.resolveClass(Class≺?≻ c)
      • 3.5.URLClassLoader类
    • 4.设计双亲委派机制的目的
    • 5.ExtClassLoader和AppClassLoader
  • 六.Tomcat 中的类加载器
    • 1.Tomcat类加载器类型说明
    • 2.Tomcat为什么要打破双亲委派模型
    • 3.线程上下文类加载器
  • 七.如何打破双亲委派模型
  • 八.打破双亲委派模型的常见场景
  • 九.热部署类加载器

Jvm基本组成

JVM 的结构基本上由 5 部分组成:

  • 类加载器(ClassLoader):在 JVM 启动时或者类运行时将需要的 class文件 解析后生成一个Class对象加载到到 JVM 中
  • 执行引擎(ExcutionEngine):执行引擎的任务是负责执行 class 文件中包含的字节码指令,相当于实际机器上的 CPU
  • 内存区(Memory Area):将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等,由Heap、程序计数器、虚拟机栈、本地方法栈和方法区五部分组成。
  • 本地方法调用(Native Method Interface):调用 C 或 C++ 实现的本地方法的代码返回结果
  • 垃圾收集器(Garbage Collection):负责将内存区中无用对象的释放,主要是堆内存和方法区

JVM被分为三个主要的子系统:类加载器子系统、运行时数据区、执行引擎
在这里插入图片描述

一.什么是JVM类的加载

Java代码编译执行过程

  1. 源码编译:通过Java源码编译器(Javac命令)将Java代码编译成Jvm字节码(.class文件)
  2. 类加载:通过ClassLoader及其子类来完成Jvm的类加载
  3. 类执行:字节码被装入内存,进入Jvm,被解释器(Java命令)解释执行

在这里插入图片描述

什么是类的加载

  • 从上图来看其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,并对class文件中的数据进行校验、转换、解析、初始化等操作后,将其放在运行时数据区方法区内,然后在区创建一个 Java.lang.Class对象,封装类在方法区内的数据结构。

二.类的生命周期

类从JVM加载到卸载出内,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载 7个阶段

  • 类加载包含了前5个,其中 验证、准备、解析为连接。具体如图:
    在这里插入图片描述

阶段1:加载

  • 通过一个类的全路径查找此类字节码文件,并利用class文件创建对应的Class对象并加载到方法区

阶段2:验证

  • 确保Class文件的字节流中包含信息符合虚拟机规范,而不会危害虚拟机自身运行安全。主要包括4种验证,文件格式验证,元数据验证,字节码验证,符号引用验证

阶段3:准备

  • 为类变量(即static修饰的字段变量)分配内存并且设置该类变量的默认值
    • (如static int i=5; 这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static变量,因为final在编译的时候就会分配了,且这里不会为实例变量分配初始化类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

阶段4:解析

  • 解析阶段是将常量池中的符号引用替换为直接引用的过程。
    • 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main() 方法)替换为指向数据所存内存的指针或句柄等(直接引用),这就是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
    • 主要工作:类或接口的解析,字段解析,类方法解析,接口方法解析

例如:在com.xxx.Person类中引用了com.xxx.Animal类,在编译阶段Person类并不知道Animal的实际内存地址,因此只能用com.xxx.Animal来代表Animal真实的内存地址而在解析阶段,JVM可以通过解析该符号引用,来确定com.xxx.Animal类的真实内存地址(如果该类未被加载过,则先加载)。

符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

阶段5:初始化

初始化阶段是类加载过程的最后一步,这一步才 真正开始执行类中定义的Java程序代码

  • 即对类的静态变量初始化为指定的值,执行静态代码块

在之前的准备阶段,类中定义的static静态变量已经被赋过一次默认值。而在初始化阶段,则会调用类构造器<clinit>来完成初始化操作,为静态变量赋原始值。

  • 此处,需要注意的是类构造器<clinit>类构造方法<init>区别。
    • 类构造方法<init>每创建一次对象,就自动调用一次,而类构造器<clinit>类似于一个无参的构造函数,只不过该函数是静态修饰,只会初始化静态所修饰的代码
public class Student{
    public static int x = 10;
    public String zz = "jiaboyan";

    static{
        System.out.println("12345");
    }
}
  • 类构造器<clinit>由编译器自动收集类中的所有静态变量的赋值动作静态语句块static{}中的代码合并产生的,编译器收集的顺序是由 语句在源文件中出现的顺序所决定的

综上所述,对于上面的例子来说,类构造器<clinit>为:

public class Student{
    <clinit>{
        public static int x = 10;
        System.out.println("12345");
    }
}

在这里插入图片描述

三.类初始化时机

JVM规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列5种情况必须对类进行初始化(加载、验证、准备都会随着发生)

  1. 遇到 new、getstatic、putstatic、invokestatic 这4条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。

    最常见的生成这 4 条指令的场景是:1.使用 new 关键字实例化对象的时候;2.读取或设置类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候 3.调用一个类的静态方法的时候。

  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。

  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(启动类)(包含 main() 方法的那个类),虚拟机会先初始化这个主类;

  5. 当使用 JDK.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

以上 5 种场景中的行为称为对一个类进行主动引用除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

特殊情况

  • 当 Java 虚拟机初始化一个类时,要求它所有的父类都被初始化,但这一条规则并不适用于接口。一个父接口并不会因为他的子接口或者实现类的初始化而初始化,只有当程序首次被使用特定接口的静态变量时,才会导致该接口的初始化。

代码块、静态方法、构造方法加载顺序

public class Son extends Father{
    static {  System.out.println("子类静态代码块"); }

    {  System.out.println("子类代码块");  }

    public Son() { System.out.println("子类构造方法"); }

    public static void main(String[] args) {
        new Son();
    }

}

class Father{
    static { System.out.println("父类静态代码块"); }

    {System.out.println("父类代码块");}

    public Father() { System.out.println("父类构造方法");}

    public static void find() {
        System.out.println("静态方法");
    }
}
//代码块和构造方法执行顺序
//父类静态代码块
//子类静态代码块
//父类代码块
//父类构造方法
//子类代码块
//子类构造方法

四.类加载器

  • 类加载器的作用 : 通过类全限定名读取类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例
    在这里插入图片描述

1.引导类加载器(Bootstrap Class Loader)

  • 引导类加载器,也叫启动类加载器 主要加载的是JVM自身需要的类,该类加载使用C++实现的,是虚拟机自身的一部分,负责加载 <JAVA_HOME>\lib\ 目录下的核心类库或被 -Dbootclaspath 参数指定的类, 如: rt.jar, tool.jar 等

2.拓展类加载器(Extension Class Loader)

  • 拓展类加载器: 由Sun公司通过Java在sun.misc.Launcher$ExtClassLoader类中实现, 负责加载 <JAVA_HOME>\lib\ext 扩展目录中的类库 -Djava.ext.dirs 选项所指定目录下的类和 jar包,开发者可以直接使用标准扩展类加载器,
//Launcher$ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
     //加载<JAVA_HOME>/lib/ext目录中的类库
     String s = System.getProperty("java.ext.dirs");
     File[] dirs;
     if (s != null) {
         StringTokenizer st = new StringTokenizer(s, File.pathSeparator);
         int count = st.countTokens();
         dirs = new File[count];
         for (int i = 0; i < count; i++) {
             dirs[i] = new File(st.nextToken());
         }
     } else {
         dirs = new File[0];
     }
     return dirs;
 }

3.应用程序类加载器(System Class Loader)

  • 应用程序类加载器,也称为系统类加载器:由Sun公司通过Java在sun.misc.Launcher$AppClassLoader中实现, 负责加载 环境变量 CLASSPATH -Djava.class.path、java -cp所指定的目录下的类和 jar 包。可以通过ClassLoader#getSystemClassLoader()获取到该类加载器,开发者可以直接使用系统类加载器,该加载器是程序中默认类加载器
public class MainClassLoaderTest {

    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(DESKeyFactory.class.getClassLoader());
        System.out.println(MainClassLoaderTest.class.getClassLoader());
        /*null
        sun.misc.Launcher$ExtClassLoader@246b179d
        sun.misc.Launcher$AppClassLoader@18b4aac2*/

        System.out.println();
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();//应用程序类加载器
        ClassLoader extClassLoader = appClassLoader.getParent();//拓展类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();//引导类加载器
        System.out.println("bootstrapClassLoader: " + bootstrapClassLoader);
        System.out.println("extClassLoader: " + extClassLoader);
        System.out.println("appClassLoader: " + appClassLoader);
        /*bootstrapClassLoader: null
        extClassLoader: sun.misc.Launcher$ExtClassLoader@246b179d
        appClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2*/

        //BootstrapLoader  父加载器 及 加载路径
        System.out.println();
        System.out.println("bootstrapLoader 加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }
        /*bootstrapLoader 加载以下文件:
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/resources.jar
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/rt.jar
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/sunrsasign.jar
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jsse.jar
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jce.jar
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/charsets.jar
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/lib/jfr.jar
        file:/E:/dev_tools/jdk/jdk1.8.0_181/jre/classes*/

        //ExtClassLoader 父加载器 及 加载路径
        System.out.println();
        System.out.println("extClassLoader 加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));
        /*extClassLoader 加载以下文件:
        E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\ext;C:\Windows\Sun\Java\lib\ext*/


        //AppClassLoader父加载器 及 加载路径
        System.out.println();
        System.out.println("appClassLoader 加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));
        /*appClassLoader 加载以下文件:
        E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\charsets.jar;
        省略.....
        E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\rt.jar;
        省略
        E:\dev_tools\jdk\jdk1.8.0_181\jre\lib\resources.jar;
        省略
        E:\04_resource_study\java_base_demo\target\classes;
        省略
        E:\dev_tools\mavenLocalStorage\org\springframework\boot\spring-boot-starter-web\2.2.0.RELEASE\spring-boot-starter-web-2.2.0.RELEASE.jar;
        E:\dev_tools\mavenLocalStorage\com\alibaba\fastjson\1.2.78\fastjson-1.2.78.jar;
        E:\dev_tools\mavenLocalStorage\mysql\mysql-connector-java\8.0.18\mysql-connector-java-8.0.18.jar;*/
    }
}
  • 可看出,Launcher采用了单例设计模式,其中BootstrapClassLoader 、ExtClassLoader和AppClassLoader的扫描路径分别对应系统属性sun.boot.class.path、java.ext.dirs、java.class.path

4.自定义类加载器(Custom Class Loader)

  • 自定义类加载器:负责加载用户自定义包路径下的类包通过 ClassLoader 的子类实现 Class 的加载
  • 如何实现继承抽象类ClassLoader重写findClass方法,判断当前类的class文件是否已被加载
/**
 * 自定义类加载器
 */
public class MyClassLoader extends ClassLoader {
    //包路径
    private String path;

    //构造方法,用于初始化Path属性
    public MyClassLoader(String path) {
        this.path = path;
    }

    //重写findClass方法,参数name表示要加载类的全类名(包名.类名)
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //检查该类的class文件是否已被加载,如果已加载则返回class文件(字节码文件)对象,如果没有加载返回null
        Class<?> loadedClass = findLoadedClass(name);
        //如果已加载直接返回该类的class文件(字节码文件)对象
        if (loadedClass != null) {
            return loadedClass;
        }

        //字节数组,用于存储class文件的字节流
        byte[] bytes = null;
        try {
            //获取class文件的字节流
            bytes = getBytes(name);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (bytes == null) {
            throw new ClassNotFoundException();

        }

        //如果字节数组不为空,则将class文件加载到JVM中
        //将class文件加载到JVM中,返回class文件对象
        //字节码数组加载到 JVM 的方法区,并在 JVM 的堆区建立一个java.lang.Class对象的实例,用来封装 Java 类相关的数据和方法
        return this.defineClass(name, bytes, 0, bytes.length);
    }

    //获取class文件的字节流
    private byte[] getBytes(String name) throws Exception {
        //拼接class文件路径 replace(".",File.separator) 表示将全类名中的"."替换为当前系统的分隔符,File.separator返回当前系统的分隔符
        String fileUrl = path + name.replace(".", File.separator) + ".class";

        //缓冲区
        byte[] buffer = new byte[1024];
        //输入流
        InputStream fis = new FileInputStream(new File(fileUrl));
        //相当于一个缓存区,动态扩容,也就是随着写入字节的增加自动扩容
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        //循环将输入流中的所有数据写入到缓存区中
        int bytesNumRead = 0;
        // 读取类文件的字节码
        while ((bytesNumRead = fis.read(buffer)) != -1) {
            baos.write(buffer, 0, bytesNumRead);
        }

        baos.flush();
        baos.close();
        return baos.toByteArray();
    }


    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        String path = "E:/04_resource_study/java_base_demo/target/classes/";
        //创建自定义类加载器对象
        MyClassLoader classLoader = new MyClassLoader(path);
        System.out.println("MyDamageClassLoader的父加载器:" + classLoader.getParent());
        //返回加载的class对象
        Class<?> clazz = classLoader.findClass("com.demo.classload.ClassLoaderTest");
        //调用类的构造方法创建对象
        Object o = clazz.newInstance();
        //输出创建的对象
        System.out.println("创建的对象:"+o);

        //输出当前类加载器
        System.out.println("ClassLoaderTest当前类加载器:"+clazz.getClassLoader());
        //输出当前类加载器的父类
        System.out.println("ClassLoaderTest当前类加载器的父类:"+clazz.getClassLoader().getParent());
        //输出当前类加载器的父类的父类
        System.out.println("ClassLoaderTest当前类加载器的父类的父类:"+clazz.getClassLoader().getParent().getParent());
        //输出当前类加载器的父类的父类的父类
        System.out.println("ClassLoaderTest当前类加载器的父类的父类的父类:"+clazz.getClassLoader().getParent().getParent().getParent());

        /*
        执行结果:
        MyDamageClassLoader的父加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
        创建的对象:com.demo.classload.ClassLoaderTest@238e0d81
        ClassLoaderTest当前类加载器:com.demo.classload.MyDamageClassLoader@7daf6ecc
        ClassLoaderTest当前类加载器的父类:sun.misc.Launcher$AppClassLoader@18b4aac2
        ClassLoaderTest当前类加载器的父类的父类:sun.misc.Launcher$ExtClassLoader@31221be2
        ClassLoaderTest当前类加载器的父类的父类的父类:null
        */
    }
}

为什么要自定义类加载器?

  1. 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件
  2. 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中
  3. 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能)

5.类加载器的关系

  • JVM 的类加载器具有父子关系,但不是通过继承来实现而是每个类加载器通过组合方式维护一个 parent字段,指向父加载器
  • 启动类加载器(Bootstrap Class Loader),由C++实现,没有父类。启动类加载器无法被java程序直接引用。
  • 拓展类加载器(Extension ClassLoader),由Java语言实现,父类加载器为null(如果parent=null,则它的父级就是启动类加载器。)
  • 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
  • 自定义类加载器(Custom Class Loader),父类加载器肯定为AppClassLoader。

6.JVM类加载策略

  • 按需加载: JVM对Class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的Class文件加载到内存生成Class对象

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责一起加载,除非显示使用另外一个类加载器来载加载

  • 父类委托(双亲委派):先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类(为了避免不同的加载器 加载相同的类 出现重复字节)

  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

在JVM中表示两个class对象是否为同一个类对象存在2个必要条件

  • 类的全限定名必须一致(即包名.类名)
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
    即JVM中两个类对象(class对象)就算来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,但前提是重写loadClass(), 因为双亲委派模型第一步会通过Class c = findLoadedClass(name)从缓存查找,类全限定名相同则不会再次被加载,因此我们必须跳过缓存查询才能重新加载class对象。当然也可直接调用findClass()方法,这样也避免从缓存查找.

五.双亲委派机制

1.什么是双亲委派机制

JVM 的类加载器具有父子关系,双亲委派机制是在Java 1.2后引入的,其工作原理的是 ,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器(Bootstrap Class Loader),如果启动类加载器可以完成类加载任务,就成功返回,否则就一层一层向下委派子加载器去尝试加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?

在这里插入图片描述
如图所示

  • 当System ClassLoader拿到一个class 文件之后, 会先问父加载器Extension ClassLoader能不能加载
  • 当Extension ClassLoader 接收到请求时,会先问问他的父加载器BootStrap ClassLoader能不能加载
  • 如果 Bootstrap ClassLoader可以加载,则由Bootstrap ClassLoader来加载,如不能则由Extension ClassLoader来加载. 如果 Extension ClassLoader也加载不了的话,最后由System ClassLoader来加载.

那么采用这种模式有啥用呢?

  • 双亲委派机制保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类

2.浅析ClassLoader类

JVM 并不是在启动时就把所有的.class文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader,这个抽象类中定义了3个关键方法,理解清楚它们的作用和关系非常重要。

public abstract class ClassLoader {
    //委托的父类加载器
    private final ClassLoader parent;
    
	//name为类的全限定名(包名.类名)	
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    
    //name为类的全限定名(包名.类名)	,resolve如果为true,则在生成class对象的同时进行解析相关操作
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    	//同步控制:开启并发加载的情况下,锁对类对应的锁对象,否则为ClassLoader对象本身
        synchronized (getClassLoadingLock(name)) {
    	//首先,检查类是否已经加载
            Class<?> c = findLoadedClass(name);
             //1.如果没有被加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果有父加载器,则先委派给父加载器去加载(注意这里是递归调用)
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                     // 如果父加载器为空,则委托启动类加载器去加载 
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
					//如果没有从非空父加载器中找到类,则抛出类异常ClassNotFoundException 
                }

        		//2.如果父加载器没加载成功,调用自己实现的findClass去加载
                if (c == null) {
					//如果仍然没有找到,则调用findClass来查找类。
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    //这是定义类装入器;记录数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }

			//是否生成class对象的同时进行解析相关操作
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    
    //name为类的全限定名(包名.类名)	
    protected Class<?> findClass(String name){
       //1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
       //......省略具体实现......
       //2. 调用defineClass将字节数组转成Class对象
       return defineClass(buf, off, len)}
    
    //将字节码数组解析成一个Class对象,用native方法实现
    //b:class文件字节数组,off:开始读取位置,len:每次字节数
    protected final Class<?> defineClass(byte[] b, int off, int len){
       //......省略具体实现......
    }

     //获取类名对应的锁对象
    //ClassLoader并发加载是通过一个ConcurrentHashMap<String,Object>实现的,Key为类名,对应的Value为一个new Object(),
    //所以它可以同时加载多个类,但同一个类重复加载时则可以锁住。通过registerAsParallelCapable()可以启用并发加载。
    //详见https://blog.csdn.net/w1673492580/article/details/81912344
    protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }
}

从上面的代码可以得到几个关键信息:

  • JVM 的类加载器具有父子关系,但不是通过继承来实现而是每个类加载器通过组合方式维护一个 parent字段,指向父加载器
    • AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是BootstrapClassLoader,但是ExtClassLoader的parent=null
  • defineClass():通过调用 native 方法Java 类的字节码解析成一个 Class 对象
  • findClass():用于找到.class文件并把.class文件读到内存得到字节码数组,然后在方法内部调用 defineClass方法得到 Class 对象
    • 子类必须实现findClass
  • loadClass():主要职责就是实现双亲委派机制:首先检查类是不是被加载过,如果加载过直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载
    如图所示:
    在这里插入图片描述

3.双亲委派模式具体实现

  • 可以看出顶级类加载器是抽象类ClassLoader类,除启动类加载器外,所有的类加载器都继承自ClassLoader,这里主要介绍ClassLoader中几个比较重要的方法。

在这里插入图片描述

3.1.loadClass(String name)

该方法就是双亲委派机制的具体实现,用于加载指定全限定名的类,由ClassLoader类实现,在JDK1.2之后不建议开发者重写但可以直接通过this.getClass().getClassLoder.loadClass("className") 调用,源码如下:

  • 业务逻辑:当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载器去的父加载器去加载倘若没有父加载器则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载
	//name为类的全限定名(包名.类名)	
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    
    //name为类的全限定名(包名.类名)	,resolve如果为true,则在生成class对象的同时进行解析相关操作
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    	//同步锁
        synchronized (getClassLoadingLock(name)) {
    	//首先,检查类是否已经加载
            Class<?> c = findLoadedClass(name);
             //1.如果没有被加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果有父加载器,则先委派给父加载器去加载(注意这里是递归调用)
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                     // 如果父加载器为空,则委托启动类加载器去加载 
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
					//如果没有从非空父加载器中找到类,则抛出类异常ClassNotFoundException 
                }

        		//2.如果父加载器没加载成功,调用自己实现的findClass去加载
                if (c == null) {
					//如果仍然没有找到,则调用findClass来查找类。
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    //这是定义类装入器;记录数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }

			//是否生成class对象的同时进行解析相关操作
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

3.2.findClass(String name)-(重点)

该方法在JDK1.2之后已不建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中

  1. findClass()在loadClass()中被调用的,在loadClass()中当父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式
  2. ClassLoader类没有实现findClass()方法,只是简单的抛出ClassNotFoundException
    protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);//直接抛出异常
    }
    
  3. 在findClass()中把class文件读到内存得到字节码数组后,需要在该方法内部调用 defineClass()方法得到 Class 对象

3.3.defineClass(byte[] b, int off, int len)

该方法由ClassLoader类中实现,用于将byte字节流解析成JVM能够识别的Class对象(),通常与findClass()方法一起使用

  • 一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并重写类加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象
  • 直接调用defineClass()生成类的Class对象,该Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

代码示例

protected Class<?> findClass(String name) throws ClassNotFoundException {
      // 获取类的字节数组
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
          //使用defineClass生成class对象
          return defineClass(name, classData, 0, classData.length);
      }
  }

3.4.resolveClass(Class≺?≻ c)

  • 使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用

3.5.URLClassLoader类

URLClassLoader是ClassLoade的具体实现,并新增URLClassPath类辅助获取Class字节码流等功能,如果自定义类加载器时,没有太复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁

在这里插入图片描述

4.设计双亲委派机制的目的

  • 避免类的重复加载:当父类加载器已经加载类后子类加载器没必要再次加载,保证了类加载的唯一性。

    • 双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。
  • 保证 Java 核心库的类型安全Java 核心库中的类加载工作都是由启动类加载器统一来完成的。从而确保了Java 应用所使用的都是同一个版本的 Java 核心类库,他们之间是相互兼容的

    • java.lang.Object,存在于rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。
  • 不同的类加载器可以为相同类(binary name)的类创建额外的命名空间。相同名称的类可以并存在JVM中,只需要不同的类加载器来加载他们即可,不同的类加载器的类之间是不兼容的,这相当于在JVM内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中得到了实际运用。

5.ExtClassLoader和AppClassLoader

拓展类加载器ExtClassLoader和系统类加载器AppClassLoader都继承自URLClassLoader,是sun.misc.Launcher的静态内部类

  • sun.misc.Launcher主要被系统用于启动主应用程序

在这里插入图片描述

ExtClassLoader并没有重写loadClass()方法,这说明其遵循双亲委派模式,而AppClassLoader重载了loadCass()方法,但最终调用的还是父类loadClass()方法因此依然遵守双亲委派模式,重载方法源码如下:

/**
  * Override loadClass 方法,新增包权限检测功能
  */
 public Class loadClass(String name, boolean resolve) throws ClassNotFoundException{
     int i = name.lastIndexOf('.');
     if (i != -1) {
         SecurityManager sm = System.getSecurityManager();
         if (sm != null) {
             sm.checkPackageAccess(name.substring(0, i));
         }
     }
     //依然调用父类的方法
     return (super.loadClass(name, resolve));
 }

  • 其实无论是ExtClassLoader还是AppClassLoader都继承URLClassLoader类,因此它们都遵守双亲委托模型

六.Tomcat 中的类加载器

1.Tomcat类加载器类型说明

在这里插入图片描述

在Java类加载器基础上,Tomcat新增了几个类加载器,包括3个基础类加载器和每个Web应用的私有类加载器,其中3个基础类加载器可在conf/catalina.properties中配置,具体介绍下:

  • Common Class Loader::以System Class Loader 为父类,是tomcat顶层的公用类加载器,其路径由conf/catalina.properties中的common.loader指定,默认加载$CATALINE_HOME/lib下的包。

  • Catalina Class Loader:以Common Class Loader 为父类,是用于加载Tomcat应用服务器的类加载器,其路径由server.loader指定,默认为空,此时tomcat使用Common Class Loader加载应用服务器。

  • Shared ClassLoader:以Common Class Loader 为父类,是所有Web应用的父加载器,其路径由shared.loader指定,默认为空,此时Tomcat使用Common Class Loader类加载器作为Web应用的父加载器。

  • Webapp ClassLoader:Shared ClassLoader为父类,加载/WEB-INF/classes目录下的未压缩的Class资源文件以及 /WEB-INF/lib目录下的jar包是各个 Webapp 私有的类加载器, 加载路径中的 class 只对当前 webapp 可见

    • 好处:每个的Context(web应用/war包)都使用独立的ClassLoader来加载web应用中的WEB-INF/libs目录下的jar包, 因此不同的web应用包不会冲突。如A应用用的是spring 4.X , B应用用的是spring 5.X , 他们可以在同一个tomcat中运行

默认情况下,Common、Catalina、Shared类加载器是同一个,但可以配置3个不同的类加载器,使他们各司其职。

2.Tomcat为什么要打破双亲委派模型

  • 我们都知道一个Tomcat 是可以部署多个 war 包的,如果部署的多个war包中依赖的Jar包是不同版本的,比如War包A依赖 Spring 4,War包B依赖 Spring5 ,这时根据双亲委派机制,Spring4首先被加载进来,那么另一个依赖 Spring 5 的 War包B在加载时就不会再去加载 Spring 5 。因为同名的原因会直接给他返回已加载过的 Spring 4 。这时会出现版本不一致的问题。因此对于 Tomcat 来说他就需要 自己实现类加载器来打破双亲委派模型,并给每一个war包去生成一个自己对应的类加载器

如何打破?

  • Tomcat在初始化的时候通过Thread.currentThread().setContextClassLoader(xx)设置成了Catalina ClassLoader,使用Catalina ClassLoader来加载Tomcat使用的类,当Tomcat加载WebApp中的类时设置成了WebappClassLoader,而WebappClassLoader重写了loadClass方法打破了双亲委派

Web应用默认的类加载顺序是(打破了双亲委派规则):

  • 先从JVM的BootStrapClassLoader中加载。
  • 加载Web应用下/WEB-INF/classes中的类。
  • 加载Web应用下/WEB-INF/lib/中的jar包中的类。
  • 加载上面定义的System路径下面的类。
  • 加载上面定义的Common路径下面的类。

如果在配置文件中配置了<Loader delegate="true"/>,那么就是遵循双亲委派规则,加载顺序如下:

  • 先从JVM的BootStrapClassLoader中加载。
  • 加载上面定义的System路径下面的类。
  • 加载上面定义的Common路径下面的类。
  • 加载Web应用下/WEB-INF/classes中的类。
    加载Web应用下/WEB-INF/lib/中的jar包中的类。

3.线程上下文类加载器

线程上下文类加载器(ContextClassLoader)是JDK 1.2开始引入一种类加载器传递机制。可以通过Thread.currentThread().setContextClassLoader(xx)方法给一个线程设置上下文类加载器,在该线程后续执行过程中就能通过Thread.currentThread().getContextClassLoader()获取该类加载器使用。

  • 该加载器的出现就是为了方便破坏双亲委派,如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器默认线程的上下文类加载器是系统类加载器(AppClassLoader), 在线程中运行的代码可以通过类加载器来加载类和资源

七.如何打破双亲委派模型

双亲委派模型是Java设计者推荐给开发者们的默认类加载器实现方式。这个委派和加载顺序是非强制性,可破坏的。主要有2种方式

  1. 自定义类加载器,继承ClassLoader,重写findClass(),然后再重写loadClass()改变双亲委派的类加载顺序。

  2. 通过线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作。

下面使用方式1,基于代码 4.自定义类加载器(Custom Class Loader)重写loadClass()即可

public class MyClassLoader extends ClassLoader {
 
 	//......................省略其他代码......................

	//重点: 重写loadClass()改变双亲委派的类加载顺序
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException{
        //1.破坏双亲委派的位置:自定义类加载机制先委派给ExtClassLoader加载,ExtClassLoader再委派给BootstrapClassLoader,如果都加载不了,然后自定义类加载器加载,自定义类加载器加载不了才交给SystemClassLoader
        ClassLoader classLoader = getSystemClassLoader();
        while (classLoader.getParent() != null) {
            classLoader = classLoader.getParent();
        }
        Class<?> clazz = null;
        try {
            clazz = classLoader.loadClass(name);
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        if (clazz != null) {
            return clazz;
        }

        //2.自己加载
        clazz = this.findClass(name);
        if (clazz != null) {
            return clazz;
        }

        // 3.自己加载不了,再调用父类loadClass,保持双亲委派模式
        return super.loadClass(name);
    }

 	//......................省略其他代码......................


    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        String path = "E:/04_resource_study/java_base_demo/target/classes/";
        //创建自定义类加载器对象
        MyClassLoader classLoader = new MyClassLoader(path);
        System.out.println("MyDamageClassLoader的父加载器:" + classLoader.getParent());
        //返回加载的class对象
        Class<?> clazz = classLoader.findClass("com.demo.classload.ClassLoaderTest");
        //调用类的构造方法创建对象
        Object o = clazz.newInstance();
        //输出创建的对象
        System.out.println("创建的对象:"+o);

        //输出当前类加载器
        System.out.println("ClassLoaderTest当前类加载器:"+clazz.getClassLoader());
        //输出当前类加载器的父类
        System.out.println("ClassLoaderTest当前类加载器的父类:"+clazz.getClassLoader().getParent());
        //输出当前类加载器的父类的父类
        System.out.println("ClassLoaderTest当前类加载器的父类的父类:"+clazz.getClassLoader().getParent().getParent());
        //输出当前类加载器的父类的父类的父类
        System.out.println("ClassLoaderTest当前类加载器的父类的父类的父类:"+clazz.getClassLoader().getParent().getParent().getParent());

       /*
        执行结果:
        MyDamageClassLoader的父加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
        创建的对象:com.demo.classload.ClassLoaderTest@2e5d6d97
        ClassLoaderTest当前类加载器:com.demo.classload.MyDamageClassLoader@685f4c2e
        ClassLoaderTest当前类加载器的父类:sun.misc.Launcher$AppClassLoader@18b4aac2
        ClassLoaderTest当前类加载器的父类的父类:sun.misc.Launcher$ExtClassLoader@238e0d81
        ClassLoaderTest当前类加载器的父类的父类的父类:null
        null
        */
    }
}

在这里插入图片描述

重写loadClass()后, 类加载时先委派给ExtClassLoader加载,ExtClassLoader再委派给BootstrapClassLoader,如果都加载不了,然后自定义类加载器加载,自定义类加载器加载不了才交给SystemClassLoader

为什么不能直接让自定义类加载器加载呢?

  • 不能!双亲委派的破坏只能发生在SystemClassLoader及其以下的加载委派顺序,ExtClassLoader上面的双亲委派是不能破坏的!

    • 因为任何类都是继承自顶级类java.lang.Object,而加载一个类时,也会加载继承的类,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器
  • 如ClassLoaderTest类只隐式继承了Object,自定义MyDamageClassLoader 加载了ClassLoaderTest,也会加载Object。 如果loadClass()直接调用MyDamageClassLoader的findClass()会报错java.lang.SecurityException: Prohibited package name: java.lang

八.打破双亲委派模型的常见场景

目前比较常见的场景主要有:

  1. 线程上下文类加载器(Thread.currentThread().setContextClassLoader(xx)、Thread.currentThread().getContextClassLoader()),如:JDBC 使用线程上下文类加载器加载 Driver 实现类

  2. Tomcat 的多 Web 应用程序

  3. OSGI 实现模块化热部署

九.热部署类加载器

热部署类加载器: 即利用同一个class文件不同的类加载器在内存创建出两个不同的class对象

  • JVM在加载类之前会检测请求类是否已加载过(即在loadClass()方法中调用findLoadedClass()方法),如果被加载过,则直接从 缓存 获取,不会重新加载。

  • 前面的自定义类加载器MyClassLoader通过直接调用findClass()方法已具备这个热加载功能

    • 为什么要调用findClass()来实现热加载,而不是loadClass()呢?
      • ClassLoader中默认实现的loadClass()方法中调用findLoadedClass()方法进行了检测是否已被加载,因此直接调用findClass()方法就可以绕过重用class缓存问题, 当然也可以重写loadClass()方法,但强烈不建议这么干
  • 注意; 同一个class文件最多只能被同一个类加载器的实例调用findClass()加载一次,多次加载将报错, 因此必须让同一类加载器的不同实例加载同一个class文件,以实现所谓的热部署。

public class FileClassLoader extends ClassLoader {
    /**
     * 根路径
     */
    private String rootDir;


    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }


    /**
     * 重写findClass逻辑
     *
     * @param className
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {

        //获取类的class字节数组,用于存储class文件的字节流
        byte[] bytes = null;
        try {
            //获取class文件的字节流
            bytes = getBytes(className);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //如果文件为空
        if (bytes == null) {
            throw new ClassNotFoundException();
        }

        //直接生成class对象
        return defineClass(className, bytes, 0, bytes.length);
    }


    //获取class文件的字节流
    private byte[] getBytes(String className) throws IOException {
        // 读取类文件的字节
        String path = classNameToPath(className);

        //缓冲区
        byte[] buffer = new byte[1024];
        //输入流
        InputStream fis = new FileInputStream(new File(path));
        //相当于一个缓存区,动态扩容,也就是随着写入字节的增加自动扩容
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        //循环将输入流中的所有数据写入到缓存区中
        int bytesNumRead = 0;
        // 读取类文件的字节码
        while ((bytesNumRead = fis.read(buffer)) != -1) {
            baos.write(buffer, 0, bytesNumRead);
        }

        baos.flush();
        baos.close();
        return baos.toByteArray();
    }


    /**
     * 类文件的完全路径
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        //拼接class文件路径 replace(".",File.separator) 表示将全类名中的"."替换为当前系统的分隔符,File.separator返回当前系统的分隔符
        return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    }
}

测试方法

    public static void main(String[] args) {
        //类所在包的绝对路径
        String rootDir = "E:/04_resource_study/java_base_demo/target/classes/";
        //类的全限定名
        String classPath = "com.demo.classload.ClassLoaderTest";

        //创建自定义文件类加载器
        FileClassLoader loaderA = new FileClassLoader(rootDir);
        FileClassLoader loaderB = new FileClassLoader(rootDir);

        try {
            //加载指定的class文件,调用loadClass()
            Class<?> objectL1 = loaderA.loadClass(classPath);
            Class<?> objectL2 = loaderB.loadClass(classPath);
            System.out.println("loadClass->object1:" + objectL1.hashCode());
            System.out.println("loadClass->object2:" + objectL2.hashCode());

            Class<?> object3 = loaderA.loadClass(classPath);
            Class<?> object4 = loaderB.loadClass(classPath);
            System.out.println("loadClass->obj3:" + object3.hashCode());
            System.out.println("loadClass->obj4:" + object4.hashCode());


            Class<?> object5 = loaderA.findClass(classPath);
            Class<?> object6 = loaderB.findClass(classPath);
            System.out.println("findClass->object5:" + object5.hashCode());
            System.out.println("findClass->object6:" + object6.hashCode());


            //执行到这里会报错: java.lang.LinkageError: loader (instance of  com/demo/classload/FileClassLoader): attempted  duplicate class definition for name: "com/demo/classload/ClassLoaderTest"
            //原因: 同一个class文件最多只能被同一个类加载器的实例调用`findClass()`加载一次,多次加载将`报错`, 因此必须让`同一类加载器的不同实例加载同一个class文件`,以实现所谓的热部署。
            // Class<?> object7 = loaderA.findClass(classPath);
            // Class<?> object8 = loaderB.findClass(classPath);
            
            FileClassLoader loaderC= new FileClassLoader(rootDir);
            FileClassLoader loaderD = new FileClassLoader(rootDir);
            Class<?> object7 = loaderC.findClass(classPath);
            Class<?> object8 = loaderD.findClass(classPath);
            System.out.println("findClass->object7:" + object7.hashCode());
            System.out.println("findClass->object8:" + object8.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        // 执行结果
        // loadClass->object1:1751075886
        // loadClass->object2:1751075886
        // loadClass->obj3:1751075886
        // loadClass->obj4:1751075886
        
        // findClass->object5:930990596
        // findClass->object6:1921595561
        // findClass->object7:87285178
        // findClass->object8:610998173
    }

结论:

  • 同一个类加载器不同实例调用loadClass()加载class,每次返回的都是同一个class实例
  • 同一个类加载器实例调用findClass()加载class,第2次调用会抛出异常 java.lang.LinkageError, 因此每次热部署都要new一个新类加载器实例来调用findClass()加载class,以返回不同的class实例

热部署加载监控线程

  • 如何测试: 热部署监控线程启动时,只需要我们调整type()方法中,println打印内容,然后将重新编译后的Pay.class文件放到指定类加载路径中就行了
package com.demo.classload;
public class Pay {
    public void type() {
        System.out.println("微信支付");
    }
}
@Slf4j
public class HotDeploymentTest {
    public static void main(String[] args) throws InterruptedException {
        //记录文件上次修改时间
        AtomicReference<Long> lastModified = new AtomicReference<>(0L);
        new Thread(() -> {
            while (true) {

                //类文件所在包根路径
                String rootDir = "E:/04_resource_study/java_base_demo/target/classes/";
                //类文件所在包绝对路径
                String classAbsolutePath = rootDir + "/com/demo/classload/Pay.class";
                //类的全限定名
                String classSourcePath = "com.demo.classload.Pay";

                try {
                    File file = new File(classAbsolutePath);

                    //文件不存在,线程休眠
                    if (!file.exists()) {
                        int sleep = 2000;
                        log.info("文件{}不存在,休眠{}ms", file.getAbsolutePath(), sleep);
                        TimeUnit.MILLISECONDS.sleep(sleep);
                        continue;
                    }

                    //获取文件的上次修改时间
                    Long modified = file.lastModified();

                    //如果文件上次修改时间发生改变,使用类加载器重载文件
                    if (modified > lastModified.get()) {
                        //当前文件的文件上次修改时间
                        lastModified.set(modified);
                        log.info("文件{}存在且发生改变,lastModified:{},modified:{},开始热部署", file.getAbsolutePath(), lastModified.get(), modified);

                        //创建自定义文件类加载器
                        FileClassLoader loader = new FileClassLoader(rootDir);
                        //加载指定的class文件,直接调用findClass(),绕过检测机制,创建不同class对象。
                        Class<?> clazz = loader.findClass(classSourcePath);

                        //调用类的构造方法创建对象
                        Object obj = clazz.newInstance();

                        //获取该对象的方法
                        String methodName = "type";
                        Method method = clazz.getMethod(methodName, null);

                        //执行方法
                        method.invoke(obj,  null);
                    }
                    //没有改变,线程休眠
                    else {
                        log.info("文件{}存在未发生改变,lastModified:{},modified:{}", file.getAbsolutePath(), lastModified.get(), modified);
                        TimeUnit.MILLISECONDS.sleep(5000);
                    }


                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }
}

执行结果
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/394420.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

持久化:利用Linux PAM创建后门

目录 Linux PAM详解 使用PAM创建SSH后门密码 利用PAM记录密码 利用PAM免密登录 Linux PAM详解 PAM&#xff08;Pluggable Authentication Modules&#xff0c;可插入的身份验证模块&#xff09;是Linux自带的一套与身份验证机制相关的库&#xff0c;可以将多种身份验证的方…

5、Linux 常用指令

一、帮助指令 1.man 指令 语法 man [命令或配置文件] //功能描述&#xff1a;获得帮助手册上的信息查看 ls 命令的帮助信息 man ls信息作用NAME命令名称SYNOPSIS如何使用命令DESCRIPTION描述命令SEE ALSO相关的手册 2.help 指令 语法 help [命令] //功能描述&#xff1a;获得…

elementui 中 el-date-picker 控制选择当前年之前或者之后的年份

文章目录 需求分析 需求 对 el-date-picker控件做出判断控制 分析 给 el-date-picker 组件添加 picker-options 属性&#xff0c;并绑定对应数据 pickerOptions html <el-form-item label"雨量年份&#xff1a;" prop"date"><el-date-picker …

vm centos7 docker 安装 mysql 5.7.28(2024-02-18)

centos系统版本 [rootlocalhost mysql5.7]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) docker版本 拉取指定版本镜像 docker pull mysql:5.7.28 docker images 创建挂载目录&#xff08;数据存储在centos的磁盘上&#xff09; mkdir -p /app/softwa…

保护您的数据:如何应对.mallab勒索病毒的数据加密?

导言&#xff1a; 数据安全已经成为企业和个人都必须高度重视的问题。然而&#xff0c;网络犯罪分子的不断进化使得数据安全变得更加严峻。其中一种常见的威胁是勒索软件&#xff0c;而.dataru勒索病毒就是其中之一。本文将介绍.dataru勒索病毒的特征&#xff0c;以及如何恢复…

java实现排序算法(上)

排序算法 冒泡排序 时间和空间复杂度 要点 每轮冒泡不断地比较比较相邻的两个元素,如果它们是逆序的,则需要交换它们的位置下一轮冒泡,可以调整未排序的右边界,减少不必要比较 代码 public static int[] test(int[] array) {// 外层循环控制遍历次数for (int i 0; i <…

【DDD】学习笔记-领域服务

聚合的三个问题 按照面向对象设计原则&#xff0c;需要将“数据与行为封装在一起”&#xff0c;避免将领域模型对象设计为贫血对象。如此一来&#xff0c;聚合内的实体与值对象承担了与其数据相关的领域行为逻辑。聚合满足了领域概念的完整性、独立性与不变量&#xff0c;体现…

小白水平理解面试经典题目LeetCode 1025 Divisor Game【动态规划】

1025 除数游戏 小艾 和 小鲍 轮流玩游戏&#xff0c;小艾首先开始。 最初&#xff0c;黑板上有一个数字 n 。在每个玩家的回合中&#xff0c;该玩家做出的动作包括&#xff1a; 选择任意 x&#xff0c;使 0 < x < n 和 n % x 0 。将黑板上的数字 n 替换为 n - x 。 此…

【HarmonyOS】【DevEco ohpm ERROR: NOTFOUND package “@ohos/hypium“如何解决

参考 &#xff1a;&#xff08;无效&#xff09; 华为开发者论坛 DevEco创建项目时的错误解决_6 月 优质更文活动_路北路陈_InfoQ写作社区 解决&#xff1a; HormonyOS-DevEco Studio新建空项目ERROR解决_oh_modules\ohos\hypium-CSDN博客 将 .ohpm文件夹中的hypium文件夹复…

如何引导llm为自己写prompt生成剧本

如何使用写prompt让你自己生一个狗血修仙穿越短剧&#xff0c;且短剧有趣生动让人流连忘返 好的&#xff0c;我会尝试编写一个狗血修仙穿越短剧的prompt&#xff0c;以激发你的想象力&#xff0c;让你创作出一个既有趣又生动的短剧。以下是我的prompt&#xff1a; 标题&#x…

gem5学习(23):经典缓存——Classic Caches

目录 一、Interconnects 1、Crossbars 二、Debugging 默认缓存是一个带有MSHR&#xff08;未命中状态保持寄存器&#xff09;和WB&#xff08;写缓冲区&#xff09;的非阻塞缓存&#xff0c;用于读取和写入未命中。缓存还可以启用预取&#xff08;通常在最后一级缓存中&…

java.sql.SQLException: No operations allowed after statement closed.

背景 某天下午&#xff0c;客服反馈线上服务出现问题&#xff0c;不能分配了。于是我登录到系统上&#xff0c;进行同样的操作发现也不行。当然同时我已经登录到服务器打开了日志&#xff0c;发现报错了&#xff0c;下面就是日志的错误信息&#xff1a; java.sql.SQLExceptio…

Swagger-的使用

Swagger-的使用 前言效果1、相关依赖2、相关注解2.1 Tag设置整个类的名称和详情2.2 Operation描述具体的方法2.3 Parameter 描述参数2.4Schema 为属性添加注释 3、Docket配置3.1通过gropeediopenapi进行分组3.2 通过docsOpenApi设置 前言 在我们和前端进行交互的时候&#xff…

ChatGPT高效提问—prompt实践(白领助手)

ChatGPT高效提问—prompt实践&#xff08;白领助手&#xff09; ​ 随着社会的不断发展&#xff0c;白领的比例越来越高。白领的工作通常较为繁忙&#xff0c;需要管理复杂的项目。工作量大、要求高、任务紧急&#xff0c;时间分配不当部分可能导致工作效率低下&#xff0c;任…

【AGI视频】Sora的奇幻之旅:未来影视创作的无限可能

在五年后的未来&#xff0c;科技的发展为影视创作带来了翻天覆地的变化。其中&#xff0c;Sora视频生成软件成为了行业的翘楚&#xff0c;引领着全新的创作潮流。Sora基于先进的Transformer架构&#xff0c;将AI与人类的创造力完美结合&#xff0c;为观众带来了前所未有的视听盛…

OpenAI:Sora视频生成模型技术报告(中文)

概述 视频生成模型作为世界模拟器 我们探索视频数据生成模型的大规模训练。具体来说&#xff0c;我们在可变持续时间、分辨率和宽高比的视频和图像上联合训练文本条件扩散模型。我们利用transformer架构&#xff0c;在视频和图像潜在代码的时空补丁上运行。我们最大的模型Sor…

使用倒模耳机壳UV树脂胶液制作HIFI耳机隔音降噪耳机壳有哪些缺点?

虽然使用倒模耳机壳UV树脂胶液制作HIFI耳机隔音降噪耳机壳有很多优点&#xff0c;但也存在一些缺点和需要注意的事项&#xff1a; 技术要求高&#xff1a;制作过程需要一定的技术和经验&#xff0c;如模具制作、树脂混合和填充等。如果没有足够的经验和技巧&#xff0c;可能会…

浅谈js事件机制

事件是什么&#xff1f;事件模型&#xff1f; 原始事件模型&#xff08;DOM0级&#xff09; HTML代码中指定属性值&#xff1a;在js代码中指定属性值&#xff1a;优点&#xff1a;缺点&#xff1a; IE 事件模型DOM2事件模型 对事件循环的理解 宏任务&#xff08;Macrotasks&…

WSL安装Ubuntu22.04,以及深度学习环境的搭建

安装WSL 安装 WSL 2 之前&#xff0c;必须启用“虚拟机平台”可选功能。 计算机需要虚拟化功能才能使用此功能。 以管理员身份打开 PowerShell 并运行&#xff1a; dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart下载 Linux 内核更…

【开源】SpringBoot框架开发服装店库存管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 角色管理模块2.3 服装档案模块2.4 服装入库模块2.5 服装出库模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 角色表3.2.2 服装档案表3.2.3 服装入库表3.2.4 服装出库表 四、系统展示五、核心代码5.…
最新文章