【JRebel 作者出品--譯文】Java class 熱更新:關於物件,類,類載入器
一篇大神的譯文,勉強(嗯。。相當勉強)地放在類載入器系列吧,第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?
- RJC301: Classloaders in Web Development — Tomcat, GlassFish, OSGi, Tapestry 5 and so on
- RJC401: HotSwap and JRebel — Behind the Scenes
- RJC501: How Much Does Turnaround Cost?
管中窺豹
談論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。
譯文參考及原始碼:
- Internals of Java Class Loading
- Download full source code
三、總結
大神的作品,不說了。大家肯定沒耐心等我翻該系列的後續了(嗯,水平也差。。。哈哈),等不及的同學請直接去瞻仰大神的文章吧。
&n