【JRebel 作者出品--譯文】Java class 熱更新:關於物件,類,類載入器

三國夢迴發表於2019-06-27

一篇大神的譯文,勉強(嗯。。相當勉強)地放在類載入器系列吧,第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。

譯文參考及原始碼:

三、總結

 大神的作品,不說了。大家肯定沒耐心等我翻該系列的後續了(嗯,水平也差。。。哈哈),等不及的同學請直接去瞻仰大神的文章吧。

 

相關文章