一、类的生命周期

类从被加载到虚拟机内存中 开始 到 卸载出内存 为止,它的整个生命周期可以简单概括为 7 个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
其中,验证、准备和解析 这三个阶段可以统称为
链接(Linking)
。
二、类的加载过程

类加载
主要分为 5 个过程:- 加载(Loading): 加载二进制然后转换成 JVM 需要的结构,最后生成对应 Class 对象。
- 验证(Verification): 文件格式验证、元数据验证、字节码验证、符号引用验证。
- 准备(Preparation): 为 类的静态变量 分配内存并设置初始值。
- 解析(Resolution): Java 虚拟机将 常量池内的符号引用 替换为 直接引用 的过程。
- 初始化(Initialization): 就是执行类构造器 clinit()方法 的过程。
- 其中,验证、准备和解析 这三个阶段可以统称为
链接(Linking)
。
1. 加载(Loading)阶段
加载分为 3 个阶段:
- 通过类的全限定名或者类的二进制字节流, JVM 并没有规定字节流一定要用某种方式。
- 可以通过 压缩包(jar、war 包等)。
- 从网络上获取 动态代理生成、其他文件(JSP)、数据库、加密文件(防反编译) 等。
- 将字节流所代表的 静态存储结构 转化为 方法区的运行时数据结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为 方法区这个类的各种数据的访问入口。
2. 验证(Verification)阶段
验证阶段分成 4 个验证过程:
- 文件格式验证:验证字节流 是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理, 验证字节流的文件结构是否正确。
- 元数据验证:结构正确了,这一步就是 验证内容是否正确,比如里面定义的数据类型是不是都属于 JVM 所规定。
- 字节码验证:验证字节码文件 方法中的 Code 结构,就是验证所 编写的方法是否正确合理。是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。。
- 符号引用验证:校验行为发生在虚拟机 将符号引用 转化为 直接引用 的时候,这个转化动作将在解析阶段中发生,验证是否 有访问某些外部类、方法、字段的权限。
整个过程可以理解为:整体结构、结构类型、结构内容、外部引用 4 个步骤。
3. 准备(Preparation)阶段
- 为 类的静态变量 分配内存并 设置初始值。
- 我们都知道类中的静态变量是与类一起的,并不需要初始化类的对象进行访问,所以在这个阶段把这些变量所使用的内存在方法区中进行分配,
- 方法区是一个逻辑上的区域,在 JDK 7 及之前,HotSpot 使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在 JDK 8 及之后,类变量则会随着 Class 对象一起存放在 Java 堆中。
4. 解析(Resolution)阶段
- 虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:class 文件中常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 这几个结构所存储的字符串常量。
- 直接引用:能定位到所引用的真正内容。
5. 初始化(Initialization)阶段
- 就是执行类构造器clinit()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
- 在准备阶段已经对常量值设置初始值,在这里就是对常量设置用户定义的值。
- 比如在类中存在如下一行代码: public static final int i = 5;
- 在准备阶段是令 i=0,而在初始化阶段则是令 i=5 的过程。这个过程也是静态代码块的执行过程。
对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
- 当遇到 new 、 getstatic 、 putstatic 或 invokestatic 这 4 条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
- 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
- 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("...") , newInstance() 等等。如果类没初始化,需要触发其初始化。
- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
- MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 findStaticVarHandle 来初始化要调用的类。
- 当一个接口中定义了 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
读取到的文件目录
- ${JAVA_HOME}/jre/lib/**
- ${JAVA_HOME}/jre/classes
判断 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.String 类 rt.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;
}
}
- 先查询当前加载器,是否已加载到内存中。
java
Class<?> c = findLoadedClass(name);
- 未加载到内存中,向父类加载器路径包下找。
java
if (parent != null) {
// 3. 父类递归加载器
c = parent.loadClass(name, false);
}
- 父类未查询到,向启动类加载器路径包下找。
java
c = findBootstrapClassOrNull(name);
- 如果启动类加载器未找到,跳出递归,查询当前类的 findClass
java
c = findClass(name);
五、热部署插件
- 编写类加载文件
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;
}
}
- 热部署类插件文件
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();
}
}
- 编写自定义加载器
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();
}
}
实现思路
- 通过多线程(线程池),监听 Class 文件变化。
java
// 1.多线程(线程池): 定时监听 / 循环监听 文件变化
Thread thread = new Thread(() -> {}).start();
- 通过 While 循环, 获取编译后的 Class 文件列表。
java
// 循环监听
while (true) {
// 2.读取: Class文件列表
File fileDir = new File(fileDirPath);
File[] classFiles = fileDir.listFiles();
// 3.遍历:存入Map
for (File classFile : classFiles) {
}
}
- 通过 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);
}
}
- 编写测试类 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
查询编译后的 Class,文件路径: /Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-classLoader-hot-deploy/target/classes/com/calvin/hotDeploy/MyClassLoader.class
编写测试热部署功能, 复制以上文件路径, 执行线程
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();
}
- 修改 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
- 热部署测试线程输出
log
# 输出日志
class 文件已发送了变化: class com.calvin.hotDeploy.TestHotDeploy