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
传入一个classname
和byte
数组,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
也就是项目路径
首先收到任务的类加载器并不一定是最终加载这个类的类加载器——双亲委派
首先交给谁——全盘委托
bootstrapclassloader
是c++
写的,不存在于我们的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();
}
}
- 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;
}
}