java热加载

JAVA热加载

热加载

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

defineclass传入一个classnamebyte数组,byte数组是对应class文件的二进制数据数组,来对应class文件加载进虚拟机并生成class对象

findloadedclass,从当前classloader中获取一个class,如果已经加载过的class,它会存在于这个classloader的缓存中,jvm会记录它的全限定类名与对应的classloader,由这个方法返回。

注意:同一个类被不同的类加载器加载出来的对象并不相同

全盘委托:jvm决定首先由谁来开始加载

bootstrapclassloader:主要加载%JAVA_HOME%/lib,如util,lang

extclassloader:主要加载%JAVA_HOME%/lib/ext目录

appclassloader:系统类加载器,加载classpath也就是项目路径

首先收到任务的类加载器并不一定是最终加载这个类的类加载器——双亲委派

首先交给谁——全盘委托

bootstrapclassloaderc++写的,不存在于我们的java之中,

其他两个是Launcher的子类,它们是继承了一个URLClassLoader->SecureClassLoader->abstract ClassLoader

有一个loadClass方法
在这里插入图片描述

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;
    }
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    // 在里面new 了一个Object,放入了一个concurrenthashmap中<classname,object>,锁的是这个object
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 加锁
        // 如果有一个类已经被当前classloader加载的话,就不再往上抛了。它是一个native方法,类似于缓存
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 递归,双亲委派的机制
                    c = parent.loadClass(name, false);
                } else {
                    // 如果最终为空,就直接到bootstrapclassloader里面去加载
                    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。这个方法是我们去加载类的
                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;
    }
}

那么怎么让jvm去加载我们的类生成对象呢,这就需要依靠defineclass这个方法

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

那么我们自己来系欸给热加载的方法。

那么其实我们也发现了在刚刚的loadClass方法中它是因为有一个缓存,它会先去findLoadedClass方法去找这个类是否已经被加载,如果我们要重新加载的话就需要跳过这个缓存。

它这个缓存其实是基于classloader来的,那么其实我们每一次都自己new一个classloader就不需要走它这个缓存了。但是我们面临的一个问题就是 双亲委派问题,就是指我们new的这个classloader不一定就由这个classloader去加载。

我们可以自己手动的xxClassLoader.loadClass("xxx")去加载一个类

但是当我们new的时候,是jvm帮我们去load的,这才跟全盘委托有关系

那么有两种方法:

1.重写loadclass方法

2.在执行loadclass之前,强制去load我们的class进去

那么我们现在使用第二种方式

package top.p3wj;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: Aaron
 * @Description:
 * @Date: Created in 20:56 2020/12/15
 */
public class MyClassLoader extends ClassLoader {
    // 项目根目录
    public String rootPath;
    // 需要加载的class记录,因为有些类不需要我们来加载,比如string之类的
    public List<String> clazzs;

    /**
     * @param rootPath:   项目根目录,需要根据此目录截取class文件路径,例如D://top/p3wj/x.class
     * @param clazzPaths: 需要加载的目录
     * @return null
     * @Description 传入指定目录 热加载class文件
     * @author Aaron
     * @Datetime 2020/12/15 20:59
     */
    public MyClassLoader(String rootPath, String... clazzPaths) throws Exception {
        this.rootPath = rootPath;
        this.clazzs = new ArrayList<>();
        for (String clazzPath : clazzPaths) {
            LoadClassPath(new File(clazzPath));
        }
    }

    /**
     * @param file: 传入扫描的目录
     * @return void
     * @Description 根据目录扫描里面的class文件,并加载进jvm虚拟机
     * @author Aaron
     * @Datetime 2020/12/15 21:02
     */
    public void LoadClassPath(File file) throws Exception {
        if (file.isDirectory()) {
            for (File listFile : file.listFiles()) {
                LoadClassPath(listFile);
            }
        } else {
            String fileName = file.getName();
            String filePath = file.getPath();
            String endName = fileName.substring(fileName.lastIndexOf(".") + 1);
            if (endName.equals("class")) {
                InputStream inputStream = new FileInputStream(file);
                byte[] bytes = new byte[(int) file.length()];
                inputStream.read(bytes);
                String className = filePathToClassName(filePath);
                clazzs.add(className);
                defineClass(className, bytes, 0, bytes.length);
            }
        }
    }

    /**
     * @Description    将文件路径替换为 className
     * @param filePath:
     * @return java.lang.String
     * @author  Aaron
     * @Datetime  2020/12/15 21:09
     */
    private String filePathToClassName(String filePath) {
        String className = filePath.replace(rootPath,"").replaceAll("\\\\",".");
        className = className.substring(0,className.lastIndexOf("."));
        className = className.substring(1);
        return className;
    }

    public static void main(String[] args) throws Exception{
        String rootPath = MyClassLoader.class.getResource("/").getPath().replaceAll("%20"," ");
        rootPath = new File(rootPath).getPath();
        while (true) {
            MyClassLoader myClassLoader = new MyClassLoader(rootPath, rootPath+"/top/p3wj");
            Class<?> aClazz = myClassLoader.loadClass("top.p3wj.Test");
            Object o = aClazz.newInstance();
            aClazz.getMethod("hello").invoke(o);
            Thread.sleep(2000);
        }
    }
}

但是以上方式只是替换而已,在线上环境我们不可能还调用load或是反射去调用

在这里插入图片描述

在运行的过程中重写编译即完成了替换

那我们用new试一下其实他是不会变化的

在这里插入图片描述

并未全部变成11111111

那么怎么让我们的new也是热加载呢?

在这里插入图片描述

我们可以看到,new使用的是appclassloader,并不是我们的myclassloader

所以我们现在要解决的就是全盘委托,看看首先用谁。然后下面看我们发现的一个问题:

我们首先在Test类中定义一个类A,按照之前的想法,`new不是由我们定义的加载器加载的

在这里插入图片描述

在这里插入图片描述

全盘委托就是我们在用new的时候,需要在我们自己定义的类加载器里面用,否则就被jvm托管了

我们自己写一个:

1.文件监听器

class FileListener extends FileAlterationListenerAdaptor {

    @Override
    public void onFileChange(File file) {
        if (file.getPath().contains(".class")) {
            try {
                // 热部署,spring的话不光会销毁对象,还会去清理自己的bean工厂,有一些复杂
                Application.close();
                MyClassLoader myClassLoader = new MyClassLoader(Application.rootPath, Application.rootPath + "/top/p3wj");
                // 必须得新启动一个,否则会走缓存
                Application.start0(myClassLoader);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void startFileMino(String rootPath) throws Exception{
        FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(rootPath);
        fileAlterationObserver.addListener(new FileListener());
        FileAlterationMonitor fileAlterationMonitor = new FileAlterationMonitor(5000);
        fileAlterationMonitor.addObserver(fileAlterationObserver);
        fileAlterationMonitor.start();
    }
}
  1. application,模仿springboot启动时的run
public class Application {
    public static String rootPath;

    public void start() throws Exception {
        // spring boot启动流程......
        init();
        new Test().hello();
    }
    public void init() {
        System.out.println("初始化项目");
    }
    public static void run(Class<?> clazz) throws Exception {
        String rootPath = clazz.getResource("/").getPath().replaceAll("%20"," ");
        rootPath = new File(rootPath).getPath();
        Application.rootPath = rootPath;
        // 跟springboot一样,我更改了任何文件都可以自动去热加载
        startFileMino(rootPath);
        MyClassLoader myClassLoad = new MyClassLoader(rootPath, rootPath+"/top/p3wj");
        start0(myClassLoad);
    }
    public static void start0(MyClassLoader myClassLoad) throws Exception {
        Class<?> aClazz = myClassLoad.loadClass("top.p3wj.Application");
        Object o = aClazz.newInstance();
        Method start = aClazz.getMethod("start");
        start.invoke(o);
    }
    public static void close() {
        System.out.println("关闭项目");
        //通知jvm销毁已失去引用的对象(执行finalize()方法)
        System.runFinalization();
        //通知jvm gc
        System.gc();
    }
}

3.我们的myclassloader

public class MyClassLoader extends ClassLoader {
    // 项目根目录
    public String rootPath;
    // 需要加载的class记录,因为有些类不需要我们来加载,比如string之类的
    public List<String> clazzs;

    /**
     * @param rootPath:   项目根目录,需要根据此目录截取class文件路径,例如D://top/p3wj/x.class
     * @param clazzPaths: 需要加载的目录
     * @return null
     * @Description 传入指定目录 热加载class文件
     * @author Aaron
     * @Datetime 2020/12/15 20:59
     */
    public MyClassLoader(String rootPath, String... clazzPaths) throws Exception {
        this.rootPath = rootPath;
        this.clazzs = new ArrayList<>();
        for (String clazzPath : clazzPaths) {
            LoadClassPath(new File(clazzPath));
        }
    }

    /**
     * @param file: 传入扫描的目录
     * @return void
     * @Description 根据目录扫描里面的class文件,并加载进jvm虚拟机
     * @author Aaron
     * @Datetime 2020/12/15 21:02
     */
    public void LoadClassPath(File file) throws Exception {
        if (file.isDirectory()) {
            for (File listFile : file.listFiles()) {
                LoadClassPath(listFile);
            }
        } else {
            String fileName = file.getName();
            String filePath = file.getPath();
            String endName = fileName.substring(fileName.lastIndexOf(".") + 1);
            if (endName.equals("class")) {
                InputStream inputStream = new FileInputStream(file);
                byte[] bytes = new byte[(int) file.length()];
                inputStream.read(bytes);
                String className = filePathToClassName(filePath);
                clazzs.add(className);
                defineClass(className, bytes, 0, bytes.length);
            }
        }
    }

    /**
     * @param filePath:
     * @return java.lang.String
     * @Description 将文件路径替换为 className
     * @author Aaron
     * @Datetime 2020/12/15 21:09
     */
    private String filePathToClassName(String filePath) {
        String className = filePath.replace(rootPath, "").replaceAll("\\\\", ".");
        className = className.substring(0, className.lastIndexOf("."));
        className = className.substring(1);
        return className;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值