上一篇文章:JVM之我们的代码是如何被加载到JVM执行的?(一)
我们讲到了常见的类加载器,以及类加载器的工作原理,我们也可以自定义类加载器来加深理解。
让我们来一个自定义类加载器
主要步骤:
- 继承ClassLoader
- 重写findClass()方法
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
ClassLoader parent = getParent();
System.out.println("parent == " + parent);
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClass(name);
if (result == null) {
throw new ClassNotFoundException();
} else {
// 生成Class对象
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClass(String name) {
try {
if (Objects.isNull(name) || name.length() == 0) return null;
int index = name.lastIndexOf(".");
String path = this.path;
if(index == -1){
path = path + name + ".class";
}else{
path = path + name.substring(index+1) + ".class";
}
return Files.readAllBytes(Paths.get(path));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
// 将生成的Study.class文件放在E盘目录下
// 然后将项目中的Study.java和Study.class删除-->将Study类认为是非本项目的代码文件
// 通过本地的路径,加载项目之外的Study.class文件
public class JVMTest {
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("E:/");
Class<?> clazz1 = loader1.findClass("com.tomato.Study");
Study o = (Study)clazz1.newInstance();
}
}
即使同一个Class文件加载出来的Class对象,JVM也可能认为是两个不同的class
如果我们没有将项目中Study.java删除,将会报异常:ClassCastException,明明是同一个Class文件加载,为什么转换的时候会是类型转换异常!!!
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。
我们既然已经自定义了类加载器,但是除了上面我们手动加载一个类的场景之外,JVM是什么时候加载Class文件的?是不是所有的Class文件都加载出来?如果一下全加载进来,启动速率肯定受影响,那怎么优化呢?
类加载什么时候加载Class文件?
项目启动时,并不是在把所有Class全部加载到内存中的,采用的是LazyLoading方式,按需加载。 其中有五个必须加载类的情况。
- new getstatic putstatic invokestatic 指令,访问final除外,因为final在编译为class文件时就确定好了
- java.lang.reflect 对类反射调用时
- 初始化子类是,父类必定被先加载
- 虚拟机启动时,需要执行的主类需要加载
- 动态语言支持java.lang.invoke.MethodHandle解析的结果为REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄时,该类被初始化。
上面我们知晓了类加载器加载Class文件的时机,下面则是解答加载的三种执行模式。
JVM的三种执行模式
之前有说过Java文件从开始编译到最后执行的流程:1.xxx.java -> 2.javac编译 -> 3.xxx.class -> 4.ClassLoader -> 5.字节码解释器、JIT -> 6.执行引擎执行
其中第五步就涉及到执行模式,调整JVM可以更改执行模式(默认混合模式)
- 解释模式
通过解释器(Bytecode Interpreter)解释执行,因为不需要编译,启动很快,但是执行慢,可通过-Xin参数指定为纯解释模式。
- 编译模式
由JIT编译为本地代码(C语言实现),运行很快但是启动慢,可以用-Xcomp参数指定为纯编译模式。
- 混合模式
由于上面两种方式各有各的好,所以JVM就把两种模式都用上,并加上“热点代码编译”合称之为混合模式,起始阶段采用解释执行,然后通过热点代码检测(HotSpot),检测这段代码是否为热点代码,如果是则将它编译。可以用-Xmixed参数指定为混合模式。
在混合模式下,JVM是通过热点代码的检测来判断这段代码是否需要被编译(被优化):
- 多次被调用的方法(方法计数器:监测方法执行频率);
- 多次被调用的循环(循环计数器:监测循环执行频率);
十分特殊的“上下文类加载器”
之前的介绍的常见的类加载器:Bootstrap ClassLoader,Extention ClassLoader,App ClassLoader,Customer ClassLoader都是实际存在的,但是这个“上下文类加载器”是一个定义,它可以是上面实际中的任何一个。
设置上下文类加载器是属于线程的方法Thread.setContextClassLoader(ClassLoader cl)
官方解释:
/**
* Sets the context ClassLoader for this Thread. The context
* ClassLoader can be set when a thread is created, and allows
* the creator of the thread to provide the appropriate class loader,
* through {@code getContextClassLoader}, to code running in the thread
* when loading classes and resources.
* 为这个线程设置上下文类加载器。这个上下文类加载器可以在线程创建的时候设置(指定),也允许
* 这个线程的创建者提供适当的类加载器,通过getContextClassLoader方法在运行的线程中加载类
* 或者加载资源。
*
*/
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
我自己理解就是即可以setContextClassLoader()方法指定类加载器,如果不指定则使用当前创建这个线程的地方的类加载器(有的文章也指出是父线程的加载器,套我自己的理解,我觉得是一样的,因为当前new Thread()的线程也是当前线程的创建者,如果有不同的说法,感谢大家可以在评论下方告知我,我去学习学习)。
使用“上下文类加载器”干什么用? -- 打破双亲委派机制
由于“即使同一个Class文件加载出来的Class对象,JVM也可能认为是两个不同的class”的原因,双亲委派机制的确有各种好,但是身为(渣男)有进取心的研发人员肯定不满足“双亲”安排的机制,研发人员需要的是(自由)自定义的权利。同时我们会发现很多核心类是被Bootstrap ClassLoader加载的,但是它们的实现可能是交给其他厂商实现。例如JNDI,JDBC等Java的服务提供者接口(Service Provider Interface,SPI),核心代码被Bootstrap ClassLoader加载,但是JNDI和JDBC加载的是第三方厂商提供的实现。这种情况下,就可以使用这种机制“打破”双亲委派机制,从而有效的打破双亲委派机制带来的限制。
使用自定义类加载器,完成热插拔式功能
学了这么多理论知识,那到底我们可以使用这些理论完成哪些功能?(十年磨枪,不为出枪,难道是为了麒麟臂)
下面带大家完成一个简单的服务器热插拔式功能:修改了新代码不需要重新编译整个项目、部署等操作,只要将上传新代码至服务器,就能运行最新代码。
实现步骤
- 构建Springboot工程
- 新建NetHotSwapClassLoader.java -- 网络热插拔类加载器
- 准备两个不同版本的XXXService.java -- 用于验证热插拔功能的服务类
- 新建HotSwapController.java -- 用于上传Class文件,并最终调用Class文件中的执行代码
PS:为了让同学更加理解整个过程,以及加深所学知识,这个热插拔功能写了两版
- NetHotSwapClassLoader.java -- 网络热插拔类加载器
/**
* 网络热插拔类加载器
*/
public class NetHotSwapClassLoader extends ClassLoader {
/**
* 构造方法
*/
public NetHotSwapClassLoader() {
// 使得加载NetHotSwapClassLoader与后续NetHotSwapClassLoader加载其他类的父加载器一致
// 作用:保证类加载器一致
super(NetHotSwapClassLoader.class.getClassLoader());
}
/**
* 加载类
* @param bytes Class文件的字节流
* @return Class对象
* @throws ClassNotFoundException 异常
*/
public Class<?> loadClassBytes(byte[] bytes) throws ClassNotFoundException {
return defineClass(null, bytes, 0, bytes.length);
}
}
- XXXService.java -- 第一个版本的代码
// XXXService第一版代码
public class XXXService {
public static Object processStatic() {
// 第一版代码
return "processStatic:Hello World!";
}
public Object process() {
// 第一版代码
return "process:Hello World!";
}
}
- HotSwapController.java
@RestController
@RequestMapping("/hotSwap")
public class HotSwapController {
@PostMapping(value = "process")
public Object process(@RequestPart("file") MultipartFile[] multipartFiles) {
try {
byte[] bytes = multipartFiles[0].getBytes();
// 使用网络热插拔类加载器
NetHotSwapClassLoader classLoader = new NetHotSwapClassLoader();
// 加载类
Class<?> clazz = classLoader.loadClassBytes(bytes);
Object instance = clazz.newInstance();
// 执行上传的class文件中的代码(非现在项目中的代码)
Method method1 = clazz.getDeclaredMethod("processStatic");
Method method2 = clazz.getDeclaredMethod("process");
Object invoke1 = method1.invoke(null);
Object invoke2 = method2.invoke(instance);
System.out.println("processStatic执行结果 == " + invoke1);
System.out.println("process执行结果 == " + invoke2);
return invoke1;
} catch (Exception e) {
e.printStackTrace();
}
return "异常出现,未按预计执行";
}
@GetMapping(value = "test")
public Object test() {
// 试运行第一版代码
XXXService hotSwapService = new XXXService();
Object processStatic = XXXService.processStatic();
Object process = hotSwapService.process();
System.out.println("test processStatic执行结果 == " + processStatic);
System.out.println("test process执行结果 == " + process);
return processStatic;
}
}
准备到这一步,我们现在先通过Postman调用/hotSwap/test接口,测试现在的执行的代码,运行结果如下:
test processStatic执行结果 == processStatic:Hello World!
test process执行结果 == process:Hello World!
我们现在已经看到线上的是第一版的代码,那我们在不重新部署第二版代码的情况下,却执行第二版的代码: 现在准备第二版本的代码
- XXXService.java -- 第二个版本的代码(第二版代码可自行编写,不一定要参照我这样写,只要两个版本代码有区别就行)
public class XXXService {
public static Object processStatic() {
// 第二版 代码 通过SpringBoot生成的SbProperties
SbProperties bean = SpringContextHolder.getApplicationContext().getBean(SbProperties.class);
return bean;
}
public Object process() {
// 第二版 代码 通过SpringBoot生成的HelloService
HelloService helloService = SpringContextHolder.getApplicationContext().getBean(HelloService.class);
return helloService.test1();
}
}
将第二版代码编译获得XXXService.class文件,并使用Postman上传至接口/hotSwap/process(注意:此时启动的服务一直运行的都是第一版代码)
获得执行结果如下:
processStatic执行结果 == SbProperties(title=好好学习, desc=天天向上)
process执行结果 == service test1
细心的朋友可以在此时,如果再调用/hotSwap/test接口会发现执行的还是第一版的代码,Why?我们可以回顾之前的内容,非同一个ClassLoader加载的Class,JVM依然认为它俩不是同一个Class,所以如果要达成调用/hotSwap/test,也是最新的第二版代码,需要保证两个Class是同一个ClassLoader加载出来的,并且要将第二个版本的Class对象刷到内存当中,这样就能完成全局的Class对象生效。
让我们来实现一下第二版。
1.新写接口YService,方法定义与XXXService一致(目的是为了使用YService来规避显性使用XXXService带来的类型转换异常,例如上一版直接在test()方法里就显性使用了XXXService),然后让XXXService继承它。(由于之前的第二版代码没有继承YService导出的Class,所以需要重新实现YService,再将第二版代码重新导出!!!这步很重要,不然拿之前的Class文件上传会出问题))
public interface YService {
Object process();
}
// XXXService第一版代码(新)
public class XXXService implement YService {
public static Object processStatic() {
// 第一版代码
return "processStatic:Hello World!";
}
@Override
public Object process() {
// 第一版代码
return "process:Hello World!";
}
}
// XXXService第二版代码(新)
public class XXXService implement YService {
public static Object processStatic() {
// 第二版代码
return "Good Good Study!";
}
@Override
public Object process() {
// 第二版代码
return "Day Day UP!";
}
}
2.改造NetHotSwapClassLoader
/**
* 网络热插拔类加载器
*/
public class NetHotSwapClassLoader extends ClassLoader {
public static final Map<String, byte[]> classBytes = new HashMap<String, byte[]>();
public static final Map<String,Class<?>> classMap = new HashMap<String, Class<?>>();
/**
* 构造方法
*/
public NetHotSwapClassLoader() {
// 使得加载NetHotSwapClassLoader与后续NetHotSwapClassLoader加载其他类的父加载器一致
// 作用:保证类加载器一致
super(NetHotSwapClassLoader.class.getClassLoader());
}
/**
* 由于很多时候我们使用了很多核心类库里的东西,例如Object,String对象,
* 如果重写时不注意,很可能会发生ClassNotFoundException异常,因为根据ClassPath,这些类库都是由Bootstrap ClassLoader加载出来的。
* 自定义的ClassLoader加载的ClassPath根本没包含这个区域。
*/
// @Override
// public Class<?> loadClass(String name) throws ClassNotFoundException {
// return super.loadClass(name);
// }
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 打破双亲委派机制,不从父类加载器中寻找
byte[] buf = classBytes.get(name);
if (buf == null) {
return Class.forName(name);
}
if(classMap.containsKey(name)){
return classMap.get(name);
}
Class<?> aClass = defineClass(name, buf, 0, buf.length);
return aClass;
}
/**
* 加载类
* @param bytes Class文件的字节流
* @return Class对象
* @throws ClassNotFoundException 异常
*/
public Class<?> loadClassBytes(String name,byte[] bytes) throws ClassNotFoundException {
return defineClass(name, bytes, 0, bytes.length);
}
}
- HotSwapController.java
@RestController
@RequestMapping("/hotSwap")
public class HotSwapController {
@PostMapping(value = "process")
public Object process(@RequestPart("file") MultipartFile[] multipartFiles) {
try {
byte[] bytes = multipartFiles[0].getBytes();
NetHotSwapClassLoader.classBytes.put(XXXService.class.getName(),bytes);
// 使用网络热插拔类加载器
NetHotSwapClassLoader classLoader = new NetHotSwapClassLoader();
// 加载类
Class<?> clazz = classLoader.findClass(XXXService.class.getName());
Object instance = clazz.newInstance();
// 执行上传的class文件中的代码(非现在项目中的代码)
Method method1 = clazz.getDeclaredMethod("processStatic");
Method method2 = clazz.getDeclaredMethod("process");
Object invoke1 = method1.invoke(null);
Object invoke2 = method2.invoke(instance);
System.out.println("processStatic执行结果 == " + invoke1);
System.out.println("process执行结果 == " + invoke2);
return invoke1;
} catch (Exception e) {
e.printStackTrace();
}
return "异常出现,未按预计执行";
}
@GetMapping(value = "test")
public Object test() {
Object processStatic = null;
// 使用网络热插拔类加载器
NetHotSwapClassLoader classLoader = new NetHotSwapClassLoader();
try {
// 由于loadClass方法的双亲委派机制,我们加载Class的方法直接使用findClass
// 上面的文章也解析了源码,双亲委派机制找不到也加载不出来时,也是类加载器通过findClass加载Class
Class<?> clazz = classLoader.findClass(XXXService.class.getName());
YService hotSwapService = (YService) clazz.newInstance();
processStatic = hotSwapService.process();
} catch (Exception e) {
e.printStackTrace();
}
return processStatic;
}
}
未上传前执行的是第一版代码,上传完Class文件后,再次执行test()方法,发现执行的都是最新的代码。因为没有封装,所以看起来很冗余,可以抽取出来写成工具类,这样就比较顺眼一点。
至此,通过了解ClassLoader,完成了一个简单的热插拔式的功能。
希望通过两个版本的对比,期望能让同学们加深理解Classloader。