摘要:本文主要介绍类加载器、自定义类加载器及类的加载和卸载等内容,并举例介绍了Java类的热替换。

最近,遇到了两个和Java类的加载和卸载相关的问题:

1) 是一道关于Java的判断题:一个类被首次加载后,会长期留驻JVM,直到JVM退出。这个说法,是不是正确的?

2) 在开发的一个集成平台中,需要集成类似接口的多种工具,并且工具可能会有新增,同时在不同的环境部署会有裁剪(例如对外提供服务的应用,不能提供特定的采购的工具),如何才能更好地实现?

针对上面的第2点,我们采用Java插件化开发实现。上面的两个问题,都和Java的类加载和热替换机制有关。

1. Java的类加载器和双亲委派模型

1.1 Java类加载器

类加载器,顾名思义,就是用来实现类的加载操作。每个类加载器都有一个独立的类名称空间,就是说每个由该类加载器加载的类,都在自己的类名称空间,如果要比较两个类是否“相等”,首先这两个类必须在相同的类命名空间,即由相同的类加载器加载(即对于任何一个类,都必须由该类本身和加载它的类加载器一起确定其在JVM中的唯一性),不是同一个类加载器加载的类,不会相等。

在Java中,主要有如下的类加载器:

图1.1 Java类加载器

下面,简单介绍上面这几种类加载器:

  • 启动类加载器(Bootstrap Class Loader):这个类使用C++开发(所有的类加载器中,唯一使用C++开发的类加载器),用来加载<JAVA_HOME>/lib目录中jar和tools.jar或者使用 -Xbootclasspath 参数指定的类。
  • 扩展类加载器(Extension Class Loader):定义为misc.Launcher$ExtClassLoader,用来加载<JAVA_HOME>/lib/ext目录或者使用java.ext.dir指定的类。
  • 应用程序类加载器(Application Class Loader):定义为misc.Launcher$AppClassLoader,用来加载用户类路径下面(classpath)下面所有的类,一般情况下,该类是应用程序默认的类加载器。
  • 用户自定义类加载器(User Class Loader):用户自定义类加载器,一般没有必要,后面我们会专门来一部分介绍该类型的类加载器。

1.2 双亲委派模型

双亲委派模型,是从 Java1.2 开始引入的一种类加载器模式,在Java中,类的加载操作通过java.lang.ClassLoader中的loadClass()方法完成,咱们首先看看该方法的实现(直接从Java源码中捞出来的):

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {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();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

我们结合上面的注释,来解释下双亲委派模型的内容:

1) 接收到一个类加载请求后,首先判断该类是否有加载,如果已经加载,则直接返回;

2) 如果尚未加载,首先获取父类加载器,如果可以获取父类加载器,则调用父类的loadClass()方法来加载该类,如果无法获取父类加载器,则调用启动器加载器来加载该类;

3) 判断该类是否被父类加载器或者启动类加载器加载,如果已经加载完成则返回,如果未成功加载,则自己尝试来加载该类。

上面的描述,说明了loadClass()方法的实现,我们进一步对上面的步骤进行解释:

  • 因为类加载器首先调父类加载器来进行加载,从loadClass()方法的实现,我们知道父类加载器会尝试调自己的父类加载器,直到启动类加载器,所以,任何一个类的加载,都会最终委托到启动类加载器来首先加载;
  • 在前面有进行介绍,启动类加载器、扩展类加载器、应用程序类加载器,都有自己加载的类的范围,例如启动类加载器只加载JDK核心库,因此并不是父类加载器就可以都加载成功,父类加载器无法加载(一般如上面代码,抛出来ClassNotFoundException),此时会由自己加载。

最后啰嗦一下,再进行一下总结:

双亲委派模型:如果一个类加载器收到类加载请求,会首先把加载请求委派给父类加载器完成,每个层次的类加载器都是这样,最终所有的加载请求都传动到最根的启动类加载器来完成,如果父类加载器无法完成该加载请求(即自己加载的范围内找不到该类),子类加载器才会尝试自己加载。

这样的双亲委派模型有个好处:就是所有的类都尽可能由顶层的类加载器加载,保证了加载的类的唯一性,如果每个类都随机由不同的类加载器加载,则类的实现关系无法保证,对于保证Java程序的稳定运行意义重大。

2. Java的类动态加载和卸载

2.1 Java类的卸载

在Java中,每个类都有相应的Class Loader,同样的,每个实例对象也会有相应的类,当满足如下三个条件时,JVM就会卸载这个类:

1) 该类所有实例对象不可达

2) 该类的Class对象不可达

3) 该类的Class Loader不可达

那么,上面示例对象、Class对象和类的Class Loader直接是什么关系呢?

在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。而一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。所以,Class实例和加载它的加载器之间为双向引用关系

一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

Java虚拟机自带的类加载器(前面介绍的三种类加载器)在JVM运行过程中,会始终存在,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。因此,由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载

那么,我们是不是就完全不能在Java程序运行过程中,动态修改我们使用的类了吗?答案是否定的!根据上面的分析,通过Java虚拟机自带的类加载器加载的类无法卸载,我们可以自定义类加载器来加载Java程序,通过自定义类加载器加载的Java类,是可以被卸载的

2.2 自定义类加载器

前面介绍到,类加载的双亲委派模型,是推荐模型,在loadClass中实现的,并不是必须使用的模型。我们可以通过自定义类加载器,直接加载我们需要的Java类,而不委托给父类加载器。

图2.1 自定义类加载器

如上图所示,我们有自定义的类加载器MyClassLoader,用来加载类MyClass,则在JVM中,会存在上面三类引用(上图忽略这三种类型对象对其他的对象的引用)。如果我们将左边的三个引用变量,均设置为null,那么此时,已经加载的MyClass将会被卸载。

2.3 动态卸载存在的问题

动态卸载需要借助于JVM的垃圾收集功能才可以做到,但是我们知道,JVM的垃圾回收,只有在堆内存占用比较高的时候,才会触发。即使我们调用了System.gc(),也不会立即执行垃圾回收操作,而只是告诉JVM需要执行垃圾回收,至于什么时候垃圾回收,则要看JVM自己的垃圾回收策略。

但是我们不需要悲观,即使动态卸载不是那么牢靠,但是实现动态的Java类的热替换还是有希望的。

3. Java类的热替换

下面通过代码来介绍Java类的热替换方法(代码简陋,主要为了说明问题):

如下面的代码:

首先定义一个自定义类加载器:

package zmj;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class FileClassLoader extends ClassLoader {private String fileName;public void setFileName(String fileName) {this.fileName = fileName;}public Class loadClass(String name) throws ClassNotFoundException {if (name.startsWith("java")) {return getSystemClassLoader().loadClass(name);}Class cls = null;File classF = new File(fileName);try {cls = instantiateClass(name, new FileInputStream(classF), classF.length());} catch (IOException e) {e.printStackTrace();}return cls;}private Class instantiateClass(String name, InputStream fin, long len) throws IOException {byte[] raw = new byte[(int) len];fin.read(raw);fin.close();return defineClass(name, raw, 0, raw.length);}
}

上面在loadClass时,先判断类name(包含package的全限定名)是否以java开始,如果是java开始,则使用JVM自带的类加载器加载。

然后定义一个简单的动态加载类:

package zmj;public class SayHello {public void say() {System.out.println("hello ping...");}
}

在执行过程中,会动态修改打印内容,测试类的热加载。

然后定义一个调用类:

package zmj;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class Main {public static void main(String[] args) throws InterruptedException, ClassNotFoundException,IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {while (true) {FileClassLoader fileClassLoader = new FileClassLoader();fileClassLoader.setFileName("D:/workspace/idea/test/class-loader-test/target/classes/zmj/SayHello.class");Object obj = null;obj = fileClassLoader.loadClass("zmj.SayHello").newInstance();Method m = obj.getClass().getMethod("say", new Class[]{});m.invoke(obj, new Object[]{});Thread.sleep(2000);}}
}

当我们运行上面Main程序过程中,我们动态修改执行内容(SayHello中,从 hello zmj... 更改为 hello ping...),最终展示的内容如下:

hello zmj...
hello zmj...
hello zmj...
hello ping...
hello ping...
hello ping...

4. 总结

本文主要介绍类加载器、自定义类加载器及类的加载和卸载等内容,并举例介绍了Java类的热替换实现。

其实,最近在开发项目中,需要裁剪特性,就想用pf4j来做插件化开发,了解了一些类加载机制,整理一下。

主要参考《深入Java虚拟机:JVM高级特性与最佳实践》。

本文分享自华为云社区《Java类动态加载和热替换》,原文作者:maijun 。

点击关注,第一时间了解华为云新鲜技术~

透过现象看本质:Java类动态加载和热替换相关推荐

  1. 软件架构设计案例_透过现象看本质:常见的前端架构风格和案例

    所谓软件架构风格,是指描述某个特定应用领域中系统组织方式的惯用模式.架构风格定义一个词汇表和一组约束,词汇表中包含一些组件及连接器,约束则指出系统如何将构建和连接器组合起来.软件架构风格反映了领域中众 ...

  2. 透过现象看本质: 常见的前端架构风格和案例

    所谓软件架构风格,是指描述某个特定应用领域中系统组织方式的惯用模式.架构风格定义一个词汇表和一组约束,词汇表中包含一些组件及连接器,约束则指出系统如何将构建和连接器组合起来.软件架构风格反映了领域中众 ...

  3. 传统金融PK互联网 必须透过现象看本质

    传统金融PK互联网 必须透过现象看本质 面对越来越热的互联网金融,传统金融业内人士与互联网的发生激烈了冲突,中国近期的经济疲软似乎也变成了互联网金融的原罪之一,但事实真是如此吗? width=&quo ...

  4. 大道至简之四:透过现象看本质

    大道至简:透过现象看本质 --投资总结之四         时寒冰 研究趋势是一件充满挑战和趣味的事情.     我深信,很多现象是有规律可循的.     但是,对于趋势的判断尤其提前做出的判断,短期 ...

  5. 大道至简:透过现象看本质

    大道至简:透过现象看本质 --投资总结之四         时寒冰 研究趋势是一件充满挑战和趣味的事情.     我深信,很多现象是有规律可循的.     但是,对于趋势的判断尤其提前做出的判断,短期 ...

  6. 透过现象看本质:喧闹的中国电子商务

    吴军先生做<透过现象看本质:喧闹的中国电子商务>主题演讲,剖析中国电子商务行业的现状和格局,预测未来行业趋势,并针对传统企业.中小企业的具体情况,给出一些策略上的建议. 以下是吴军先生的现 ...

  7. ChatGPT爆火背后的原因:透过现象看本质

    ChatGPT爆火背后的原因:透过现象看本质 随着人工智能技术的快速发展,我们已经在许多领域看到了AI的身影.在最近的一段时间里,ChatGPT成为了一个引起广泛关注的现象.ChatGPT以其强大的自 ...

  8. 大道至简之六:透过现象看本质

    大道至简:透过现象看本质 --投资(趋势)总结之六       时寒冰 "两个礼物,你要哪一个?"     面对诸如此类的问题,儿童会毫不犹豫地说:"我都要!" ...

  9. 从ORA-01752的错误,透过现象看本质

    这几天开发同学反映了一个问题,有一个Java写的夜维程序,用于每天定时删除历史过期数据,3月10日之前经过了内测,但这两天再次执行的时候,有一条SQL语句一直报ORA-01752的错误,由于近期做过一 ...

最新文章

  1. SQL快速入门 ( MySQL快速入门, MySQL参考, MySQL快速回顾 )
  2. 八、梯度下降法和拟牛顿法
  3. [Spring]-各种标注-零配置
  4. ADF任务流:页面片段的托管bean范围
  5. python中的深浅拷贝
  6. 10月10日见!官方再曝OPPO K5外观配置细节:6400万超清四摄加持
  7. springboot mvc html,完美起航-SpringBoot整合MVC
  8. 手把手教你从0-1做一张酷炫驾驶舱,让老板对你赞不绝口
  9. Kubelet 对资源紧缺状况的应对
  10. 【JavsScript】webapp的优化整理
  11. php多次登录失败,PHPCMS登录后台失败次数过多解决办法
  12. 卡巴斯基一年版 送序列号
  13. extjs4 视频教程
  14. 拼多多笔试_探险家冒险和大数问题
  15. sql语句中----删除表数据的三兄弟
  16. 宠物服务平台APP开发详情
  17. mysql error code 100_Mysql 导入报错 error code:1166
  18. 5G术语(一)-NR、NSA/SA
  19. Javascript中关于创建Object对象
  20. 梁静茹晒巨肚孕照引惊叹 被疑怀双胞胎

热门文章

  1. Git submodule 知识总结
  2. Bootstrap进度条堆叠
  3. ROS笔记(23) Move_base
  4. text对应java类型_【JavaWeb】79:JavaScript和Java的关系?
  5. Makefile简易教程
  6. [数据结构]A*寻路算法
  7. 【微学堂】线上Linux服务器运维安全策略经验分享
  8. 2016030207 - sql50题练习(脚本)
  9. 一个fork的面试题 转
  10. .net下完成端口(IOCP)的实现