JVM的深入理解:由一次Quartz的定时任务引发的“A cannot cast to A”的问题

由Quartz框架引发的“A cannot cast to A”的问题


起因与问题描述

向新开的项目中添加定时任务,部署集群,添加了热加载(springboot-dev-tools),发现在转型时候出现了A cannot cast to A”的问题。自己怎么可能不认识自己???排查走起!!!
JVM的深入理解:由一次Quartz的定时任务引发的“A cannot cast to A”的问题


排查

类确实是同一类,问题会出现在哪里呢?我们可以想到,类加载器不同会导致“我不是我”的问题,所以打印debug走起!
真的是类加载器不同!能注意到,我们想要的ScheduleJobEntity类,类加载器是RestartClassLoader,这一切发生在【添加热加载(springboot-dev-tools)】之后才出现的。这时候需要复习一下springboot-dev-tools与类加载器机制的知识了。

JVM的深入理解:由一次Quartz的定时任务引发的“A cannot cast to A”的问题

 

知识回顾

 

类加载器机制

  1. 有两个术语,一个叫“定义类加载器”,一个叫“初始类加载器”。比如有如下的类加载器结构:
    • bootstrap
      • ExtClassloader
      • AppClassloader
    • -自定义clsloadr1
    • -自定义clsloadr2
      如果用“自定义clsloadr1”加载java.lang.String类,那么根据双亲委派最终bootstrap会加载此类,那么bootstrap类就叫做该类的“定义类加载器”,而包括bootstrap的所有得到该类class实例的类加载器都叫做“初始类加载器”。
  2. 所说的“命名空间”,是指jvm为每个类加载器维护的一个“表”,这个表记录了所有以此类加载器为“初始类加载器”(而不是定义类加载器,所以一个类可以存在于很多的命名空间中)加载的类的列表,所以,题目中的问题就可以解释了:
    CLTest是AppClassloader加载的,String是通过加载CLTest的类加载器也就是AppClassloader进行加载,但最终委派到bootstrap加载的(当然,String类其实早已经被加载过了,这里只是举个例子)。所以,对于String类来说,bootstrap是“定义类加载器”,AppClassloader是“初始类加载器”。根据刚才所说,String类在AppClassloader的命名空间中(同时也在bootstrap,ExtClassloader的命名空间中,因为bootstrap,ExtClassloader也是String的初始类加载器),所以CLTest可以随便访问String类。这样就可以解释“处在不同命名空间的类,不能直接互相访问”这句话了。
  3. 一个类,由不同的类加载器实例加载的话,会在方法区产生两个不同的类,彼此不可见,并且在堆中生成不同Class实例。
  4. 那么由不同类加载器实例(比如-自定义clsloadr1,-自定义clsloadr2)所加载的classpath下和ext下的类,也就是由我们自定义的类加载器委派给AppClassloader和ExtClassloader加载的类,在内存中是同一个类吗?所有继承ClassLoader并且没有重写getSystemClassLoader方法的类加载器,通过getSystemClassLoader方法得到的AppClassloader都是同一个AppClassloader实例,类似单例模式。在ClassLoader类中getSystemClassLoader方法调用私有的initSystemClassLoader方法获得AppClassloader实例,在initSystemClassLoader中:
    sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
    ...
    scl = l.getClassLoader();

AppClassloader是sun.misc.Launcher类的内部类,Launcher类在new自己的时候生成AppClassloader实例并且放在自己的私有变量loader里:

loader = AppClassLoader.getAppClassLoader(extclassloader);

值得一提的是sun.misc.Launcher类使用了一种类似单例模式的方法,即既提供了单例模式的接口getLauncher()又把构造函数设成了public的。但是在ClassLoader中是通过单件模式取得的Launcher 实例的,所以我们写的每个类加载器得到的AppClassloader都是同一个AppClassloader类实例。
这样的话得到一个结论,就是所有通过正常双亲委派模式的类加载器加载的classpath下的和ext下的所有类在方法区都是同一个类,堆中的Class实例也是同一个。

springboot-dev-tools

可以参考<a href = "https://blog.csdn.net/isea533/article/details/70495714">Spring DevTools 介绍一文,其中最关键的一句话就是

重启功能是通过使用两个类加载器实现的。 对于大多数应用程序,此方法运行良好,但有时可能会导致类加载问题。
默认情况下,IDE中的任何打开的项目都会使用“restart”类加载器加载,任何常规.jar文件将使用“base”类加载器加载。

 

解决问题

 

在项目 /resource/META-INF目录下(如果没有就创建一个)创建
spring-devtools.properties文件 加入下面代码:

restart.include.mapper=/mapper-[\w-\.]+jar
restart.include.pagehelper=/pagehelper-[\w-\.]+jar
restart.include.shiro=/shiro-[\w-\.]+jar

这里其实是
添加 jar 包到 restart 类加载器中 = 后面是具体的 jar 包名称, 或正则表达式

 
上一篇:Spring整合Quartz轻松完成定时任务


下一篇:java jvm概述及工作过程中的内存管理