自定义一个热部署的ClassLoader
类加载的流程
1. 加载
加载阶段(参考java.lang.ClassLoader的loadClass()方法)虚拟机需要完成三件事情
1. 通过类全名来获取一个定义此类的二进制字节流(没有指明一定是从class文件中获取,应该也可以从其他如网络,数据库中数据,动态生成的文件中读取)。
2. 将这个字节流代表的静态存储数据结构转化为动态运行时数据结构。
3. 在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的访问入口。
2. 验证
验证是连接阶段的第一步,这个阶段是为了确保读入的class字节流里面的内容能够符合当前运行的虚拟机的要求,且不会危害虚拟机自身的安全。验证阶段会完成4个检验动作:
1. 文件格式验证:验证字节流是否符合Class文件格式的规范; 例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析), 以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如循环、分支等。
4. 符号引用验证:确保解析动作能正确执行,比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。
目的:
1.包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
2.对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
3.对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
4.对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
3. 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。
4. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
5. 初始化
初始化主要分为 类的初始化 和 实例 的初始化
类的初始化:JVM第一次加载class文件时进行,主要是对静态变量初始化语句和静态代码块的执行
实例的初始化:实例创建时才会调用如new
, 反射中newInstance()
方法,clone()
方法,java.io.ObjectInputStream
类的getObject()
方法反序列化时。
内置的类加载器为何不能进行热部署
通过源码其实可以看出,JVM并不是一开始就加载所有的类,比如JVM在执行某段代码的时候遇到了一个Class A,先在内存中搜索,没有找到class的信息,这是JVM就会找相应的class文件中去寻找相应的Class A的信息并装入内存中,这就是所谓的类加载过程,只有第一次遇到某个需要运行的类时才会加载,且只会加载一次。这样的话类加载器便不会判断class是否被更改然后进行重新加载。
双亲委派机制
为什么叫双亲委派机制
其实这是个翻译的问题,原名是Parents Delegation
,其实翻译成父级代理
或者上溯委托
挺好的,有种迭代的感觉。
什么是双亲委派机制
JVM中提供了三层的ClassLoader:
Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
AppClassLoader:主要负责加载应用程序的主函数类
Java的类加载使用双亲委派模式,即一个类加载器(自定义加载器)在加载类时,先把这个请求委托给自己的父类(AppClassLoder)加载器去执行,如果父类加载器还存在父类加载器(ExtensionClassLoader),就继续向上委托,直到顶层的启动类加载器(BootstrapClassLoader)。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。
总结:先逐级调用最父层的加载器加载,如果没有在逐级调用子类的加载器加载。
双亲委派机制的优势
可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
总结下来就是两点
- 避免类的重复加载
- 避免java的核心API被篡改
自定义一个热部署ClassLoader
思路
- 自定义一个类加载器,破坏双亲委派模式。
- 需要有一个监听器,能够监听文件的变动,当文件重新部署后,可以得到通知,然后使用自定义的类加载器去加载它。
一个简易的实现
public class CustomHotFixClassLoader extends ClassLoader {
public CustomHotFixClassLoader(String bashDir, String classFullNames[]) throws Exception {
super(null);
for (String classFullName : classFullNames) {
StringBuffer filePath = new StringBuffer(bashDir);
filePath.append(classFullName.replace('.', File.separatorChar));
filePath.append(".class");
System.out.println("CustomHotFixClassLoader filePath : " + filePath.toString());
File classFile = new File(filePath.toString());
try (FileInputStream fileInputStream = new FileInputStream(classFile)){
byte[] raw = new byte[(int) classFile.length()];
fileInputStream.read(raw);
defineClass(classFullName, raw, 0, raw.length);
}
}
}
//其实重点就是以上部分在defineClass的时候不会判断是否加载过这个class直接覆盖
// @Override
// protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// Class clz = findLoadedClass(name);
// if(clz == null)
// clz = getSystemClassLoader().loadClass(name);
// if(clz == null)
// throw new ClassNotFoundException();
// if (resolve){
// resolveClass(clz);
// System.out.println("loadClass() resolve -> true");
// }
// return clz;
// }
}
public void customLoader() throws Exception {
CustomHotFixClassLoader customHotFixClassLoader =
new CustomHotFixClassLoader(
"D:\\CodeProjects\\IdeaProjects\\Test\\src\\",
new String[]{"com.sundae.test.dynamicloadclass.TestClass"});
Class clz = customHotFixClassLoader.loadClass("com.sundae.test.dynamicloadclass.TestClass");
Object o = clz.newInstance();
Method method = clz.getMethod("getString");
Object result = method.invoke(o);
System.out.println(result);
}//通过反射调用
可以尝试用个定时任务执行这个方法,在执行过程中改变Test.java
中的一些内容,然后javac
重新编译它生成class
,就会发现功能被动态改变了。
这只是一个演示,设计模式上和效率都有很大的问题。
需要改进的地方
- 增加配置文件
- 增加文件更改监听器
- 改进设计模式(是否可以参考Spring那样对接口自动装载的模式)
思考
- 如果class内代码是一个正在执行的重量型的任务是否会出现一些问题。
- 关于垃圾回收的问题。