透过现象看本质: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 源码中捞出来的):
我们结合上面的注释,来解释下双亲委派模型的内容:
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 类的热替换方法(代码简陋,主要为了说明问题):
如下面的代码:
首先定义一个自定义类加载器:
上面在 loadClass 时,先判断类 name(包含 package 的全限定名)是否以 java 开始,如果是 java 开始,则使用 JVM 自带的类加载器加载。
然后定义一个简单的动态加载类:
在执行过程中,会动态修改打印内容,测试类的热加载。
然后定义一个调用类:
当我们运行上面 Main 程序过程中,我们动态修改执行内容(SayHello 中,从 hello zmj... 更改为 hello ping...),最终展示的内容如下:
4. 总结
本文主要介绍类加载器、自定义类加载器及类的加载和卸载等内容,并举例介绍了 Java 类的热替换实现。
其实,最近在开发项目中,需要裁剪特性,就想用 pf4j 来做插件化开发,了解了一些类加载机制,整理一下。
主要参考《深入 Java 虚拟机:JVM 高级特性与最佳实践》。
本文分享自华为云社区《Java 类动态加载和热替换》,原文作者:maijun 。
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/8e6071931d4fb2a600e6042ec】。文章转载请联系作者。
评论