一篇大神的译文, 勉强 (嗯.. 相当勉强) 地放在类加载器系列吧, 第 8 弹:
实战分析 Tomcat 的类加载器结构 (使用 Eclipse MAT 验证) 还是 Tomcat, 关于类加载器的趣味实验了不得, 我可能发现了 Jar 包冲突的秘密重写类加载器, 实现简单的热替换 @Java web 程序员, 我们一起给程序开个后门吧: 让你在保留现场, 服务不重启的情况下, 执行我们的调试代码 @Java Web 程序员, 在保留现场, 服务不重启的情况下, 执行我们的调试代码 (JSP 方式) 不吹不黑, 关于 Java 类加载器的这一点, 市面上没有任何一本图书讲到
一, 前言
手里是锤子, 看哪里都是钉子. 最近学习类加载器的感觉就是如此, 总是在想, 利用它可以做到什么? 可以做到类隔离, 不停服务执行动态调试代码, 但是, 还能做什么呢?
毕竟, Tomcat 出到现在了, 也不支持更新某一个 class 而不重启应用(这里重启应用的意思是, 不是重启 Tomcat, 而是重新部署 webapp), 而热部署同样也是一个耗时的操作. 有经验的同学应该知道 Jrebel, 开发环境的神器, 有了它, 平时用开发机和前端同学联调, 再也不用频繁重启应用了. Jrebel 可以做到动态更新某个 class, 并且可以马上生效, 但是它的实现原理是迂回了一圈去解决这个问题的, 且会有性能上的损耗, 所以在生产环境也是不建议的(jrebel 原理参考: HotSwap 和 JRebel 原理).
按理说, Java 出现都 20 几年了, 这样的需求还没解决, 背后是有什么样的原因吗? 这里, 我找到一篇 jRebel 网站上的文章, 感觉写得很好, 这里勉强利用我的渣英语翻译一下. 如果英语底子好, 直接看原文吧.
链接: Reloading Java Classes 101: Objects, Classes and ClassLoaders
ps: 翻译到最后, 发现这篇文章就是 JRebel 的作者写的, 大家看看下面的截图:
再看看维基百科:
https://en.wikipedia.org/wiki/ZeroTurnaround
二, 正文
在这篇文章里, 我们将讨论怎么利用动态的类加载器去热更一个 Java 类. 同时, 我们会先看看, 对象, 类, 类加载器是怎么互相紧密绑在一起的, 然后再看看为了达到热更的目的, 需要做出的努力. 我们将从一个问题开始, 见微知著, 解释热更的过程, 然后通过一个特定的例子来展示这其中会遇到的问题和解决方案. 本系列文章包括:
- RJC101: Objects, Classes and ClassLoaders
- RJC201: How do Classloader leaks happen? http://www.zeroturnaround.com/blog/rjc201/
- RJC301: Classloaders in Web Development - Tomcat, GlassFish, OSGi, Tapestry 5 and so on http://www.zeroturnaround.com/blog/rjc301/
- RJC401: HotSwap and JRebel - Behind the Scenes
- RJC501: How Much Does Turnaround Cost?
管中窥豹
谈论 Java class 热更之前的第一件事, 就是理解类和对象的关系. 任何 java 代码, 都和包含在类中的方法紧密关联. 简单来说, 你可以把一个类, 想成一个方法的集合, 这些方法接收 "this" 关键字作为第一个参数.(译者: 可以把深入理解 JVM 那本书拿出来翻一下了, 见下图).
类被装载进内存, 并被赋予一个唯一标识. 在 Java API 中, 你可以通过 MyObject.class 这样的方式来获得一个 java.lang.Class 的对象, 这个对象就能唯一标识被加载的这个类.
每个被创建的对象, 都能通过 Object.class 来获得对这个类的唯一标识的引用. 当在该对象上调用一个方法时, JVM 会在内部获取到 class 引用, 并调用该 class 的方法. 也就是说, 假设 mo 是 MyObject 类的一个对象, 当你调用 mo.method()时, JVM 实际会进行类似下面这样的调用: mo.getClass().getDeclaredMethod("method").invoke(mo) (虚拟机实现并不会这样写, 但是最终的结果是一致的)
因此, 每一个对象都和它的类加载器相关联(MyObject.class.getClassloader()). classLoader 的主要作用就是去定义类的可见范围 -- 在什么地方这个类是可见的, 什么地方又是不可见的. 这样的范围控制, 允许具有相同包名及类名的类共存, 只要他们是由不同的类加载加载的. 该机制也允许在一个不同的类加载器中, 加载一个新版本的类.
类热更的主要问题在于, 尽管你可以加载一个新版本的 class, 但它却会获取到一个完全不同的唯一标识(译者: 这里的意思就是, 两个 classloader 是不一致的, 即使加载同一个 class 文件). 并且, 旧的对象依然引用的是 class 的旧版本. 因此, 当调用该对象的方法时, 其依然会执行老版本的方法.
我们假设, 我们加载了 MyObject 的一个新版本的 class, 旧版本的类, 名字为 MyObject_1, 新的为 MyObject_2.MyObject_1 中的 method() 方法会返回 "1",MyObject_2 中会返回 "2". 现在, 假设 mo2 是一个 MyObject_2 类型的对象, 那么以下是成立的:
- mo.getClass() != mo2.getClass()
- mo.getClass().getDeclaredMethod("method").invoke(mo)!= mo2.getClass().getDeclaredMethod("method").invoke(mo2)
(译者: 这两句原文里没解释. 第一句就是说, 两个的 class 对象不一致, 第二行是说, mo.method ()会返回 "1", 而 mo2. method ()会返回 "2", 当然不相等)
而接下来这句, mo.getClass().getDeclaredMethod("method").invoke(mo2) 会抛出 ClassCastException, 因为 mo 和 mo2 的 class 是不一样的.
这就意味着, 热更的解决方案, 只能是创建一个 mo2,(mo2 是 mo 的拷贝), 然后将程序内部所有引用了 mo 的地方都换成 mo2. 要理解这有多困难, 想想上次你改电话号码的时候. 改你的电话号码很简单, 难的是要让你的朋友们知道你的新号码. 改号码这个事就和我们这里说的问题一样困难(甚至是不可能的, 除非你能控制对象的创建), 而且, 所有的对象中的引用, 必须同一时刻更新.
例子展示
ps: 原标题是 Down and Dirty? 这什么意思...
我们将在一个新的类加载器中, 去加载一个新版本的 class. 这里, IExample 是一个接口, Example 是它的一个实现.
- public interface IExample {
- String message();
- int plusPlus();
- }
- public class Example implements IExample {
- private int counter;
- public String message() {
- return "Version 1";
- }
- public int plusPlus() {
- return counter++;
- }
- public int counter() {
- return counter;
- }
- }
接下来我们会去创建一个动态的类加载器, 大概是下面这样:
- public class ExampleFactory {
- public static IExample newInstance() {
- URLClassLoader tmp =
- new URLClassLoader(new URL[] {getClassPath()}) {
- public Class loadClass(String name) {
- if ("example.Example".equals(name))
- return findClass(name);
- return super.loadClass(name);
- }
- };
- return (IExample)
- tmp.loadClass("example.Example").newInstance();
- }
- }
上面这个类加载器, 继承了 URLClassLoader, 遇到 "example.Example" 类时, 会自己进行加载, 路径为: getClassPath(). 最后一句, 会加载该类, 并生成一个该类的对象.
这里的 getClassPath 在本例中, 可以返回一个硬编码的路径.
我们再创建一个测试类, 其中的 main 方法会在死循环中执行并打印出 Example class 的信息.
- public class Main {
- private static IExample example1;
- private static IExample example2;
- public static void main(String[] args) {
- example1 = ExampleFactory.newInstance();
- while (true) {
- example2 = ExampleFactory.newInstance();
- System.out.println("1)" +
- example1.message() + "=" + example1.plusPlus());
- System.out.println("2)" +
- example2.message() + "=" + example2.plusPlus());
- System.out.println();
- Thread.currentThread().sleep(3000);
- }
- }
- }
我们执行下 测试类, 可以看到以下输出:
- ) Version 1 = 3
- ) Version 1 = 0
可以看到, 这里的 Version 都是 1.(Version 1 是 example2.message() 返回的, 因为此时类没有改, 所以大家都是 Version 1).
这里, 我们假设将 Example.message() 修改一下, 改为 返回 "Version 2"(译者: 这里意思是, 改完后, 重新编译为 class, 再放到 getClassPath ()对应的路径下). 那么此时输出为:
- ) Version 1 = 4
- ) Version 2 = 0
为什么会是这个结果, Version 1 是由 example1 输出的, 所以计数器一直在累加, 状态得到了保持. 而 Version 2 的计数变回了 0, 所有的状态都丢失了.(译者: 毕竟是新加载的 class, 生成的新对象啊...)
为了修复这个问题, 我们修改了一下 Example 类:
- public IExample copy(IExample example) {
- if (example != null)
- counter = example.counter();
- return this;
- }
并修改一下, 测试类中的方法:
example2 = ExampleFactory.newInstance().copy(example2);
现在再看看结果:
- ) Version 1 = 3
- ) Version 1 = 3
将 Example.message()改成返回 "version 2" 后:
- ) Version 1 = 4
- ) Version 2 = 4
如你看到的, 尽管第二个对象的状态也得到了了更新, 但这需要我们手动修改才能做到. 不幸的是, 并没有 API 去更新一个已经存在的对象的 class, 或者去可靠地拷贝该对象的状态, 所以我们不得不去寻找复杂的解决方案.
下一篇 (译者: 原文是一个系列) 将会去探究, Web 容器, OSGI,Tapestry 5,Grails 怎么样去解决热更时保持状态的问题, 然后我们会进一步深入, 可靠 HowSwap , 动态语言, 和 Instrumentation API 是怎么工作的, 同样, 也包括 Jrebel.
译文参考及源码:
- Internals of Java Class Loading
- Download full source code
三, 总结
大神的作品, 不说了. 大家肯定没耐心等我翻该系列的后续了(嗯, 水平也差... 哈哈), 等不及的同学请直接去瞻仰大神的文章吧.
来源: https://www.cnblogs.com/grey-wolf/p/11097027.html