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

华为云 2021-01-20 21:45:13
java 透过 动态 本质 现象


摘要:本文主要介绍类加载器、自定义类加载器及类的加载和卸载等内容,并举例介绍了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 loaded
Class<?> 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 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;
}
}

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

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 。

 

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

版权声明
本文为[华为云]所创,转载请带上原文链接,感谢
https://huaweicloud.blog.csdn.net/article/details/112860488

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云