2022年面试某公司的一道真题,问如何实现热加载,本人当时一脸懵,当时我是这样回答的
面试官说,思路是对的,但是具体是怎么实现的呢,我就不会了,今天我们就说一下这道题应该如何解答,我们要从这几方面回答
双亲委派机制
引导类加载器:负责加载位于JAVA_HOME/lib下的核心类库,如rt.jar包
扩展类加载器:负责加载JAVA_HOME/lib/ext目录下或java.ext.dirs类路径下的所有类库
应用程序加载器:负责加载classpath上的类库
基本思路就是如果一个类加载器收到一个类加载请求,不会自己加载,而是向上进行委托.父类记载不了,再有儿子加载
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();
// 在父加载器无法加载时,在调用本身的findClass方法进行类加载,
// 最终调用的是URLClassLoader.findClass方法
// 真正加载类的逻辑
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;
}
}
自定义类加载器
loadClass:加载类的入口方法,其中即使遵循双亲委派机制,返回class实例
findClass:如果父类加载器不能加载的时候,由当前类加载findclass进行加载 defineClass:接收字节数组,转成class实例
public class JvmClassLoader1 {
public void getValue(){
System.out.println("JvmClassLoader1");
}
}
我们写一个自定义类加载器
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath){
this.classPath= classPath;
}
private byte[] loadByte(String name) throws IOException {
String path = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + path.concat(".class"));
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
class MyClassLoaderDemo{
public static void main(String[] args) throws Exception {
// 初始化自定义类加载器,会先初始化父类ClassLoader,
// 其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
// D盘创建 demo/com/learn/jvm 几级目录,将JvmClassLoader类的复制类JvmClassLoader1.class丢入该目录
MyClassLoader myClassLoader = new MyClassLoader("D:/demo");
Class<?> aClass = myClassLoader.loadClass("com.learn.jvm.JvmClassLoader1");
// 通过反射调用类中方法
Object object = aClass.newInstance();
Method getValues = aClass.getDeclaredMethod("getValue", null);
getValues.invoke(object);
System.out.println(aClass.getClassLoader().getClass().getName());
}
}
上面就是一个我们自定义的类加载器,首先我们要明确一个概念 全盘委托:就是使用调用类的类加载器加载当前类 因此上面我们写的类加载器打印出来的类加载器实际上是我们的应用类加载器,而不是我们自定义加载器
打破双亲委派机制
要打破双亲委派机制,我们就要重写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 t1 = System.nanoTime();
if(name.contains("com.learn.jvm")){
// 自定义加载器加载类
c = findClass(name);
}else{
// 父加载器加载
c = this.getParent().loadClass(name);
}
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这样我们最终使用的就是我们的自定义类加载器
Java热加载如何实现
此时热加载就很简单了,我们只需要看到类文件修改了,就重新加载一次就可以了,可以使用监听器进行监听文件是否变化,我们这里为了简单,就用一个定时任务去不断加载就可以,如下代码
class MyClassLoaderDemo{
private static final ScheduledExecutorService scheduledExecutorService = Executors.
newScheduledThreadPool(1);
public static void main(String[] args) throws Exception {
testLoadClass();
}
private static void testLoadClass() {
// 初始化自定义类加载器,会先初始化父类ClassLoader,
// 其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
try {
// D盘创建 demo/com/learn/jvm 几级目录,将JvmClassLoader类的复制类JvmClassLoader1.class丢入该目录
MyClassLoader myClassLoader = new MyClassLoader("/Users/wangxuan/Downloads/test/out/production/test");
Class<?> aClass = myClassLoader.loadClass("com.company.JvmClassLoader1");
// 通过反射调用类中方法
Object object = aClass.newInstance();
Method getValues = aClass.getDeclaredMethod("getValue", null);
getValues.invoke(object);
System.out.println(aClass.getClassLoader().getClass().getName());
} catch (Exception e) {
}
scheduledExecutorService.schedule(() -> {
testLoadClass();
}, 1 , TimeUnit.SECONDS);
}
springboot热部署原理
当我们使用编译器启动项目后,在编译器上修改了代码后,编译器会将最新的代码编译成新的.class文件放到classpath下;而引入的spring-boot-devtools插件,插件会监控classpath下的资源,当classpath下的资源改变后,插件会触发重启
而加入了spring-boot-devtools插件依赖后,我们自己编写的文件的类加载器org.springframework.boot.devtools.restart.classloader.RestartClassLoader,是这个工具包自定义的类加载器, 项目依赖的jar使用的是JDK中的类加载器(AppClassLoader\ExtClassLoader\引导类加载器)
在插件触发的重启中,只会使用RestartClassLoader来进行加载(即:只加载我们自己编写的文件部分)
热加载和热部署的区别