【JRebel 作者出品--译文】Java class 热更新:关于对象,类,类加载器

一篇大神的译文,勉强(嗯。。相当勉强)地放在类加载器系列吧,第8弹:

实战分析Tomcat的类加载器结构(使用Eclipse MAT验证)

还是Tomcat,关于类加载器的趣味实验

了不得,我可能发现了Jar 包冲突的秘密

一、前言

手里是锤子,看哪里都是钉子。最近学习类加载器的感觉就是如此,总是在想,利用它可以做到什么? 可以做到类隔离、不停服务执行动态调试代码,但是,还能做什么呢?

毕竟,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 类。同时,我们会先看看,对象、类、类加载器是怎么互相紧密绑在一起的,然后再看看为了达到热更的目的,需要做出的努力。我们将从一个问题开始,见微知著,解释热更的过程,然后通过一个特定的例子来展示这其中会遇到的问题和解决方案。本系列文章包括:

 

管中窥豹

谈论Java class 热更之前的第一件事,就是理解类和对象的关系。任何 java 代码,都和包含在类中的方法紧密关联。简单来说,你可以把一个类,想成一个方法的集合,这些方法接收 “this” 关键字作为第一个参数。(译者:可以把深入理解JVM那本书拿出来翻一下了,见下图。其实大家可以想想,汇编语言中,一般的指令格式都是:操作码 操作数1 操作数2 。。。操作数n,而不可能是 在操作数1上调用操作码,然后操作数2作为参数这种模式。底层没有面向对象,只有面向过程)。

 

类被装载进内存,并被赋予一个唯一标识。在 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);
    }
  }
}

 

我们执行下 测试类,可以看到以下输出:

1) Version 1 = 3
2) Version 1 = 0

可以看到,这里的 Version 都是 1。(Version 1是 example2.message() 返回的,因为此时类没有改,所以大家都是Version 1)。

 

这里,我们假设将 Example.message() 修改一下,改为 返回 “Version 2”(译者:这里意思是,改完后,重新编译为class,再放到 getClassPath ()对应的路径下)。那么此时输出为:

1) Version 1 = 4
2) 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);

 

现在再看看结果:

1) Version 1 = 3
2) Version 1 = 3

将 Example.message()改成返回 “version 2”后:

1) Version 1 = 4
2) Version 2 = 4

 

如你看到的,尽管第二个对象的状态也得到了了更新,但这需要我们手动修改才能做到。不幸的是,并没有 API 去更新一个已经存在的对象的 class,或者去可靠地拷贝该对象的状态,所以我们不得不去寻找复杂的解决方案。

下一篇(译者:原文是一个系列)将会去探究,web 容器,OSGI,Tapestry 5,Grails 怎么样去解决热更时保持状态的问题,然后我们会进一步深入,可靠HowSwap 、动态语言、和 Instrumentation API 是怎么工作的,同样,也包括 Jrebel。

译文参考及源码:

三、总结

 大神的作品,不说了。大家肯定没耐心等我翻该系列的后续了(嗯,水平也差。。。哈哈),等不及的同学请直接去瞻仰大神的文章吧。

 

posted @ 2019-06-27 17:43  三国梦回  阅读(1649)  评论(0编辑  收藏  举报