Skip to content

一、类的生命周期

引用 javaguide

类的生命周期
  • 类从被加载到虚拟机内存中 开始卸载出内存 为止,它的整个生命周期可以简单概括为 7 个阶段:

    1. 加载(Loading)
    2. 验证(Verification)
    3. 准备(Preparation)
    4. 解析(Resolution)
    5. 初始化(Initialization)
    6. 使用(Using)
    7. 卸载(Unloading)
  • 其中,验证、准备和解析 这三个阶段可以统称为 链接(Linking)

二、类的加载过程

image
  • 类加载主要分为 5 个过程:
    1. 加载(Loading): 加载二进制然后转换成 JVM 需要的结构,最后生成对应 Class 对象。
    2. 验证(Verification): 文件格式验证、元数据验证、字节码验证、符号引用验证。
    3. 准备(Preparation): 为 类的静态变量 分配内存并设置初始值。
    4. 解析(Resolution): Java 虚拟机将 常量池内的符号引用 替换为 直接引用 的过程。
    5. 初始化(Initialization): 就是执行类构造器 clinit()方法 的过程。
  • 其中,验证、准备和解析 这三个阶段可以统称为 链接(Linking)

1. 加载(Loading)阶段

加载分为 3 个阶段:

  1. 通过类的全限定名或者类的二进制字节流, JVM 并没有规定字节流一定要用某种方式。
    • 可以通过 压缩包(jar、war 包等)
    • 从网络上获取 动态代理生成、其他文件(JSP)、数据库、加密文件(防反编译) 等。
  2. 将字节流所代表的 静态存储结构 转化为 方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为 方法区这个类的各种数据的访问入口

2. 验证(Verification)阶段

验证阶段分成 4 个验证过程:

  1. 文件格式验证:验证字节流 是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理, 验证字节流的文件结构是否正确。
  2. 元数据验证:结构正确了,这一步就是 验证内容是否正确,比如里面定义的数据类型是不是都属于 JVM 所规定。
  3. 字节码验证:验证字节码文件 方法中的 Code 结构,就是验证所 编写的方法是否正确合理。是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。。
  4. 符号引用验证:校验行为发生在虚拟机 将符号引用 转化为 直接引用 的时候,这个转化动作将在解析阶段中发生,验证是否 有访问某些外部类、方法、字段的权限

整个过程可以理解为:整体结构、结构类型、结构内容、外部引用 4 个步骤。

3. 准备(Preparation)阶段

  • 类的静态变量 分配内存并 设置初始值
  • 我们都知道类中的静态变量是与类一起的,并不需要初始化类的对象进行访问,所以在这个阶段把这些变量所使用的内存在方法区中进行分配,
  • 方法区是一个逻辑上的区域,在 JDK 7 及之前,HotSpot 使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在 JDK 8 及之后,类变量则会随着 Class 对象一起存放在 Java 堆中。

4. 解析(Resolution)阶段

  • 虚拟机将常量池内的符号引用替换为直接引用的过程
  • 符号引用:class 文件中常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 这几个结构所存储的字符串常量。
  • 直接引用:能定位到所引用的真正内容。

5. 初始化(Initialization)阶段

引用 javaguide

  • 就是执行类构造器clinit()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
  • 在准备阶段已经对常量值设置初始值,在这里就是对常量设置用户定义的值。
    • 比如在类中存在如下一行代码: public static final int i = 5;
    • 在准备阶段是令 i=0,而在初始化阶段则是令 i=5 的过程。这个过程也是静态代码块的执行过程。

对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 当遇到 new 、 getstatic 、 putstatic 或 invokestatic 这 4 条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("...") , newInstance() 等等。如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 findStaticVarHandle 来初始化要调用的类。
  6. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

三、类加载器

  • 类加载器: 将 .java 文件编译后的 .Class文件,读取到 内存中
  • 类加载器: 加载 -> 验证 -> 准备 -> 解析 -> 初始化 五个阶段。
  • 类加载器: 当一个类加载器收到请求之后,
    • 首先, 会依次向上查找到 最顶层类加载器(启动类加载器)
    • 依次, 向下加载 class 文件,如果已经加载到 class 文件,子加载器 不会加继续加载 该 class 文件。
  • 类加载器: 主要分为四种加载器: 启动类-加载器扩展类-加载器应用类-加载器用户自定义-加载器

BootstrapClassLoader 启动类-加载器

  • 获取启动类加载器文件目录,System.getProperty("sun.boot.class.path")
java
/**
 * 启动类-加载器
 */
public static void bootstrapClassLoader() {
    // 获取 ${JAVA_HOME}/jre/lib 或 classes 目录
    String property = System.getProperty("sun.boot.class.path");
    List<String> paths = Arrays.asList(property.split(":"));
    paths.forEach(path -> System.out.println("启动类-加载器目录:" + path));
}

public static void main(String[] args) {
    // 读取: 启动类-加载器
    bootstrapClassLoader();
}
  • 控制台输出日志
log

启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/resources.jar
启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/rt.jar
启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/sunrsasign.jar
启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jsse.jar
启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jce.jar
启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/charsets.jar
启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jfr.jar
启动类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/classes
  1. 读取到的文件目录

    • ${JAVA_HOME}/jre/lib/**
    • ${JAVA_HOME}/jre/classes
  2. 判断 java.lang.String 类 rt.jar 包,在哪个类加载器?

    • java.lang.String 类 rt.jar 包,返回为 null 说明在 BootstrapClassLoader 启动类加载器。
    • 启动类加载器底层是通过 C 语言编写。
java
package com.calvin.jvm.classLoader.util;


import org.junit.Test;

/**
 * @author Calvin
 * @date 2023/4/7
 * @since v1.0.0
 */
public class JvmClassLoaderTest {

  @Test
  public void bootstrapClassLoaderTest() {
    // 1. 判断 java.lang.String 类 rt.jar 包,在哪个类加载器?
    String s = new String();
    ClassLoader classLoader = JvmClassLoaderUtils.getClassLoader(s.getClass());
    System.out.println("1.【java.lang.String 类 rt.jar 包, 所属类加载器】: " + (classLoader == null ? "BootstrapClassLoader 应用类加载器" : classLoader));
  }

}
  • 控制台输出日志
log

1.【java.lang.Stringrt.jar 包, 所属类加载器】: BootstrapClassLoader 应用类加载器

ExtensionClassLoader 扩展类-加载器

  • 获取扩展类加载目录: System.getProperty("java.ext.dirs");
java
/**
 * 扩展类-加载器
 */
public static void extClassLoader() {
   // 获取 ${JAVA_HOME}/jre/lib/ext 目录
   String property = System.getProperty("java.ext.dirs");
   List<String> paths = Arrays.asList(property.split(":"));
   paths.forEach(path -> System.out.println("扩展类-加载器目录:" + path));
}

public static void main(String[] args) {
    // 读取:扩展类-加载器
    extClassLoader();
}
  • 控制台输出日志
log
扩展类-加载器目录:/Users/calvin/Library/Java/Extensions
扩展类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext
扩展类-加载器目录:/Library/Java/Extensions
扩展类-加载器目录:/Network/Library/Java/Extensions
扩展类-加载器目录:/System/Library/Java/Extensions
扩展类-加载器目录:/usr/lib/java

1. 读取到的文件目录

  • ${JAVA_HOME}/jre/lib/ext

ApplicationClassLoader 应用类-加载器

  • 获取 classpath 目录 System.getProperty("java.class.path");
java
/**
 * 应用类-加载器
 */
public static void appClassLoader() {
   // 获取 classpath 目录
   String property = System.getProperty("java.class.path");
   List<String> paths = Arrays.asList(property.split(":"));
   paths.forEach(path -> System.out.println("应用类-加载器目录:" + path));
}

public static void main(String[] args) {
    // 读取:应用类-加载器
    appClassLoader();
}
  • 控制台输出日志
log
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/charsets.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/dnsns.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/jaccess.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/legacy8ujsse.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/localedata.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/nashorn.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/openjsse.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/sunec.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/zipfs.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jce.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jfr.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jsse.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/management-agent.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/resources.jar
应用类-加载器目录:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/rt.jar
应用类-加载器目录:/Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-classLoader/target/classes
应用类-加载器目录:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar

1. 读取到的文件目录

  • ${JAVA_HOME}/jre/lib/: 读取 JRE 下的 lib 文件目录。
  • ${JAVA_HOME}/jre/lib/ext: 读取 JRE 下的 ext 文件目录。
  • 项目/target/: 读取项目 target 目录。

Maven 和 Java 区别:

  • maven 读取的文件目录为: 项目/target/**
  • Java 读取的文件目录为: 项目/out/**

2. 判断在项目新建 User 类包,在哪个类加载器?

java
package com.calvin.jvm.classLoader.util;


import com.calvin.jvm.classLoader.entity.User;
import org.junit.Test;

/**
 * @author Calvin
 * @date 2023/4/7
 * @since v1.0.0
 */
public class JvmClassLoaderTest {

    /**
     * 测试: 应用类-加载器
     */
    @Test
    public void appClassLoaderTest() {
        // 2. 判断 com.calvin.jvm.classLoader.entity.User 类 项目包,在哪个类加载器?
        User user = new User();
        ClassLoader classLoader2 = JvmClassLoaderUtils.getClassLoader(user.getClass());
        System.out.println("2.【com.calvin.jvm.classLoader.entity.User 类 项目包, 所属类加载器】: " + (classLoader2 == null ? "BootstrapClassLoader 启动类加载器" : classLoader2));
    }

}
log
# 控制台输出日志
2.【com.calvin.jvm.classLoader.entity.User 类 项目包, 所属类加载器】: sun.misc.Launcher$AppClassLoader@18b4aac2

CustomClassLoader 用户自定义-加载器

1. 新建自定义加载器, 从文件路径读取 Class 文件。

  • 继承 ClassLoader,从写 findClass方法内容
java
package com.calvin.jvm.classLoader.custom;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

/**
 * 自定义: 我的类加载器
 *
 * @author calvin
 * @date 2023/04/07
 */
@Data
@AllArgsConstructor
public class MyClassLoader extends ClassLoader {

  /** 文件 */
  private File file;

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    try {
      // 从文件中读取去class文件 (可以修改网络传输文件)
      byte[] data = getClassFileBytes(this.file);
      return defineClass(name, data, 0, data.length);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * 获取文件字节流
   *
   * @param file 文件
   * @return {@link byte[]}
   * @throws Exception 异常
   */
  private byte[] getClassFileBytes(File file) throws Exception {
    // 采用NIO读取
    FileInputStream fis = new FileInputStream(file);
    FileChannel fileC = fis.getChannel();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    WritableByteChannel outC = Channels.newChannel(baos);
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true) {
      int i = fileC.read(buffer);
      if (i == 0 || i == -1) {
        break;
      }
      buffer.flip();
      outC.write(buffer);
      buffer.clear();
    }
    fis.close();
    return baos.toByteArray();
  }

}
java
package com.calvin.jvm.classLoader.util;


import com.calvin.jvm.classLoader.custom.MyClassLoader;
import org.junit.Test;

import java.io.File;

/**
 * @author Calvin
 * @date 2023/4/7
 * @since v1.0.0
 */
public class JvmClassLoaderTest {

    /**
     * 测试-自定义类加载器
     */
    @Test
    public void customClassLoaderTest() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        // 获取文件
        File file = new File("/Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-classLoader/remote/classes/com/calvin/jvm/classLoader/entity/User.class");
        // 将文件放入自定义加载器
        MyClassLoader myClassLoader = new MyClassLoader(file);
        // 通过自定义加载器,获取文件对应的Class
        Class<?> aClass = myClassLoader.loadClass("com.calvin.jvm.classLoader.entity.User");
        // 实例化
        Object o = aClass.newInstance();
        System.out.println("3.【通过自定义加载器以文件形式获取Class, 所属类加载器】: " + o.getClass().getClassLoader());
    }

}
  • 控制台输出日志
log

3.【通过自定义加载器以文件形式获取Class, 所属类加载器】: MyClassLoader(file=/Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-classLoader/remote/classes/com/calvin/jvm/classLoader/entity/User.class)

三、类加载器加载规则

  • JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。 也就是说,大部分类在具体 用到的时候才会去加载,这样对内存更加友好。
  • 对于已经加载的类会被放在 ClassLoader 中。
    • 在类加载的时候,系统会首先判断当前类是否被加载过。
    • 已经被加载的类会直接返回,否则才会尝试加载。
    • 也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

四、源码分析

核心源码类 ClassLoader.java

java
    private final ClassLoader parent;

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 1. 判断是否加载过Class
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 2. 当前类加载器是否存在父类
                    if (parent != null) {
                        // 3. 父类递归加载器
                        c = parent.loadClass(name, false);
                    } else {
                        // 4. 否则使用启动类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                // 5. 加载器还是为空
                if (c == null) {
                    long t1 = System.nanoTime();
                    // 6. 调用当前类加载器,查询Class文件
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  1. 先查询当前加载器,是否已加载到内存中。
java
 Class<?> c = findLoadedClass(name);
  1. 未加载到内存中,向父类加载器路径包下找。
java
 if (parent != null) {
    // 3. 父类递归加载器
    c = parent.loadClass(name, false);
 }
  1. 父类未查询到,向启动类加载器路径包下找。
java
  c = findBootstrapClassOrNull(name);
  1. 如果启动类加载器未找到,跳出递归,查询当前类的 findClass
java
c = findClass(name);

五、热部署插件

  1. 编写类加载文件 ClassFile.java
java
package com.calvin.hotDeploy;

/**
 * 类文件
 *
 * @author Calvin
 * @date 2023/4/14
 * @since v1.0.0
 */
public class ClassFile {

    /**
     * 类名称
     */
    private String className;

    /**
     * Class
     */
    private Class c;

    /**
     * 最新被修改时间
     */
    private Long latestUpdateTime;

    /**
     * 类文件
     *
     * @param className        类名
     * @param latestUpdateTime 最新更新时间
     */
    public ClassFile(String className, Long latestUpdateTime) {
        this.className = className;
        this.latestUpdateTime = latestUpdateTime;
    }

    /**
     * 类文件
     *
     * @param className        类名
     * @param c                Class
     * @param latestUpdateTime 最新更新时间
     */
    public ClassFile(String className, Class c, Long latestUpdateTime) {
        this.className = className;
        this.c = c;
        this.latestUpdateTime = latestUpdateTime;
    }


    /**
     * 获取-类名
     *
     * @return {@link String}
     */
    public String getClassName() {
        return className;
    }

    /**
     * 设置-类名
     *
     * @param className 类名
     */
    public void setClassName(String className) {
        this.className = className;
    }

    /**
     * 获取-Class
     *
     * @return {@link Class}
     */
    public Class getC() {
        return c;
    }

    /**
     * 设置-Class
     *
     * @param c c
     */
    public void setC(Class c) {
        this.c = c;
    }

    /**
     * 获取-最新更新时间
     *
     * @return {@link Long}
     */
    public Long getLatestUpdateTime() {
        return latestUpdateTime;
    }

    /**
     * 设置-最新更新时间
     *
     * @param latestUpdateTime 最新更新时间
     */
    public void setLatestUpdateTime(Long latestUpdateTime) {
        this.latestUpdateTime = latestUpdateTime;
    }

}
  1. 热部署类插件文件 HotDeployPlugins.java
java
package com.calvin.hotDeploy;


import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 热部署类插件
 *
 * @author Calvin
 * @date 2023/4/14
 * @since v1.0.0
 * @description 主要作用: 是如何加载最新变化的class文件。
 *
 */

public class HotDeployPlugins {

    /**
     * 存放Class文件
     */
    private ConcurrentHashMap<String, ClassFile> nameToClassMap = new ConcurrentHashMap<>();

    /**
     * 文件夹路径
     */
    @Getter
    @Setter
    private String fileDirPath;

    /**
     * 类包
     */
    @Getter
    @Setter
    private String classPackage;


    HotDeployPlugins(String fileDirPath, String classPackage) {
        this.fileDirPath = fileDirPath;
        this.classPackage = classPackage;
    }

    /**
     * 监听
     *
     * @decription 实现思路:
     */
    public void listening() {
        // 1.多线程(线程池): 定时监听 / 循环监听 文件变化
        Thread thread = new Thread(() -> {
            // 循环监听
            while (true) {

                // 2.读取: Class文件列表
                File fileDir = new File(fileDirPath);
                File[] classFiles = fileDir.listFiles();

                if (classFiles == null) {
                    continue;
                }

                // 3.遍历:存入Map
                for (File classFile : classFiles) {

                    String fileName = classFile.getName();
                    if (StringUtils.isBlank(fileName)) {
                        continue;
                    }

                    long l = classFile.lastModified();
                    String className = classPackage + "." + fileName.replace(".class", "");

                    ClassFile findClassFile = nameToClassMap.get(className);
                    // 未被加载过,存入MAP
                    if (findClassFile == null) {
                        nameToClassMap.put(className, new ClassFile(className, l));
                    }

                    // 已被加载过,通过时间判断是否被修改过,设置最新时间、class文件类
                    else if (!findClassFile.getLatestUpdateTime().equals(l)) {
                        findClassFile.setLatestUpdateTime(l);

                        MyClassLoader classLoader = new MyClassLoader(classFile);
                        try {
                            Class<?> aClass = classLoader.loadClass(className);
                            Object o = aClass.newInstance();
                            System.out.println("class 文件已发送了变化: " + o.getClass());
                        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                            throw new RuntimeException(e);
                        }
                    }

                    // 预防CPU过度飙高
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
    }

}
  1. 编写自定义加载器 MyClassLoader.java, 从文件中读取去 class 文件 (可以修改网络传输文件)
java
package com.calvin.hotDeploy;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

/**
 * 自定义: 我的类加载器
 *
 * @author calvin
 * @date 2023/04/07
 */
@Data
@AllArgsConstructor
public class MyClassLoader extends ClassLoader {

    /** 文件 */
    private File file;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 从文件中读取去class文件 (可以修改网络传输文件)
            byte[] data = getClassFileBytes(this.file);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取文件字节流
     *
     * @param file 文件
     * @return {@link byte[]}
     * @throws Exception 异常
     */
    private byte[] getClassFileBytes(File file) throws Exception {
        // 采用NIO读取
        FileInputStream fis = new FileInputStream(file);
        FileChannel fileC = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel outC = Channels.newChannel(baos);
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        while (true) {
            int i = fileC.read(buffer);
            if (i == 0 || i == -1) {
                break;
            }
            buffer.flip();
            outC.write(buffer);
            buffer.clear();
        }
        fis.close();
        return baos.toByteArray();
    }

}

实现思路

  1. 通过多线程(线程池),监听 Class 文件变化。
java
  // 1.多线程(线程池): 定时监听 / 循环监听 文件变化
  Thread thread = new Thread(() -> {}).start();
  1. 通过 While 循环, 获取编译后的 Class 文件列表。
java
 // 循环监听
while (true) {
    // 2.读取: Class文件列表
    File fileDir = new File(fileDirPath);
    File[] classFiles = fileDir.listFiles();
    // 3.遍历:存入Map
    for (File classFile : classFiles) {

    }
}
  1. 通过 Java API 文件修改时间,判断文件是否存在变动。
    • 如果未被加载过,存入 Map(包路径+类名作为 key -> 类文件 ClassFile)
    • 如果被加载过并且被修改了,加载到自定义加载器 classLoader,通过 loadClass 方法进行对象实例,写入到加载器中。
java
 String fileName = classFile.getName();
  if (StringUtils.isBlank(fileName)) {
      continue;
  }

  long l = classFile.lastModified();
  String className = classPackage + "." + fileName.replace(".class", "");

  ClassFile findClassFile = nameToClassMap.get(className);
  // 未被加载过,存入MAP
  if (findClassFile == null) {
      nameToClassMap.put(className, new ClassFile(className, l));
  }

  // 已被加载过,通过时间判断是否被修改过,设置最新时间、class文件类
  else if (!findClassFile.getLatestUpdateTime().equals(l)) {
      findClassFile.setLatestUpdateTime(l);

      MyClassLoader classLoader = new MyClassLoader(classFile);
      try {
          Class<?> aClass = classLoader.loadClass(className);
          Object o = aClass.newInstance();
          System.out.println("class 文件已发送了变化: " + o.getClass());
      } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
          throw new RuntimeException(e);
      }
  }
  1. 编写测试类 TestHotDeploy.java
java
package com.calvin.hotDeploy;

import java.util.Date;

/**
 * @author Calvin
 * @date 2023/4/14
 * @since v1.0.0
 */
public class TestHotDeploy {

  public static void main(String[] args) {
    System.out.println("热部署功能: 我未变更之前" + System.currentTimeMillis());
  }
}
log
# 输出日志
热部署功能: 我未变更之前1682301262280
  1. 查询编译后的 Class,文件路径: /Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-classLoader-hot-deploy/target/classes/com/calvin/hotDeploy/MyClassLoader.class

  2. 编写测试热部署功能, 复制以上文件路径, 执行线程

java
    public static void main(String[] args) {
        HotDeployPlugins hotDeployPlugins = new HotDeployPlugins(
                "/Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-classLoader-hot-deploy/target/test-classes/com/calvin/hotDeploy",
                "com.calvin.hotDeploy");
        // 监听发送变化
        hotDeployPlugins.listening();
    }
  1. 修改 TestHotDeploy,再执行一次,输出日志。
java
package com.calvin.hotDeploy;


/**
 * @author Calvin
 * @date 2023/4/14
 * @since v1.0.0
 */
public class TestHotDeploy {

    public static void main(String[] args) {
        System.out.println("热部署功能: 我未变更之后 "+ System.currentTimeMillis());
    }
}
log
# 输出日志
热部署功能: 我未变更之后 1682303054835
  1. 热部署测试线程输出
log
# 输出日志
class 文件已发送了变化: class com.calvin.hotDeploy.TestHotDeploy