HotSwap和JRebel原理 轉

HotSwap和JRebel原理

HotSwap和Instrumentation

在2002年的時候,Sun在Java 1.4的JVM中引入了一種新的被稱作HotSwap的實驗性技術,這一技術被合成到了Debugger API內部,其允許調試者使用同一個類標識來更新類的字節碼。這意味着所有對象都可以引用一個更新後的類,並在它們的方法被調用的時候執行新的代碼,這就避免了無論何時只要有類的字節碼被修改就要重載容器的這種要求。所有新式的IDE(包括Eclipse、IDEA和NetBeans)都支持這一技術,從Java 5開始,這一功能還通過Instrumentation API直接提供給Java應用使用。

hotswap

不幸的是,這種重定義僅限於修改方法體——除了方法體之外,它既不能添加方法或域,也不能修改其他任何東西。這限制了HotSwap的實用性,且其還因其他的一些問題而變得更糟:

Java編譯器常常會創建合成的方法或是域,儘管你僅是修改了一個方法體(比如說,在添加一個類字面常量(class literal)、匿名的和內部的類的時候等等)。 在調試模式下運行常常會降低應用的速度或是引入其他的問題。

這些情況導致了HotSwap很少被使用,較之應該可能被使用的頻度要低。

爲什麼HotSwap僅限於對方法體起作用?

自從引入了HotSwap之後,在最近的10年,這一問題已經被問了非常多次。在支持做整組改變的JVM調用的bug中,這是一個得票率最高的bug ,但到目前爲止,這一問題一直沒有被落實。

一個聲明:我不能說是一個JVM專家,我對JVM是如何實現的在總體上有着一個很好的理解,這幾年來我有和少數幾個(前)Sun工程師談過,不過我並沒有驗證我在這裏說的每一件事情。不過話雖如此,對於這個bug依然處開發狀態的原因我確實是有一些想法的(不過如果你更清楚其中的原因的話,歡迎指正)。

JVM是一種做了重度優化的軟件,運行在多個平臺上。性能和穩定性是其最高的優先事項。爲了在不同的環境中支持這些事項,Sun的JVM提供了這樣的功能特色:

 兩個重度優化的即時編譯器(-client和-server)
 幾個多代(multi-generational )垃圾收集器

這些功能特性使得類模式(schema)的發展變成了一個相當大的挑戰。爲了理解這其中的原因,我們需要稍微靠近一點看一看,到底是需要用什麼來支持方法和域的添加操作(甚至更深入一些,修改繼承的層次結構)。

在被加載到JVM中時,對象是由內存中的結構來表示的,結構佔據了某個特定大小(它的域加上元數據)的連續的內存區域。爲了添加一個域,我們需要調整結構的大小,但因爲臨近的區域可能已被佔用,我們就需要把整個結構重新分配到一個不同的區域中,這一區域中有足夠可用的空間來把它填寫進來。現在,由於我們實際上是更新了一個類(並不僅是某個對象),所以我們不得不對該類的每一個對象都做這樣的一件事。

這本身並不難實現——Java垃圾收集器就已經是隨時都在做重分配對象的工作的了。問題是,一個“堆”的抽象就僅是一個抽象而已。內存的實際佈局取決於當前活動的垃圾收集器,而且,爲了能與所有這些對象兼容,重分配應該有可能會被委派給活動的垃圾收集器。JVM在重分配期間還需要掛起,因此其在此期間同時進行GC工作也是合理的。

添加一個方法並不要求更新對象的結構,但確實是需要更新類的結構的,這也會體現在堆上。不過考慮一下這種情況:從類被載入之後的那一刻起,其從本質上來說就是被永久凍結了的。這使得JIT(Just-In-Time)能夠完成JVM執行的主要優化操作——內聯。應用程序熱點中的大多數方法調用會被取消,這些代碼會被拷貝到對其做調用的方法中。一個簡單的檢測會被插進來,用以確保目標對象確實是我們所認爲的對象。

於是就有了這樣可笑的事:在我們能夠添加方法到類中的時候,這種“簡單的檢查”是不夠的。我們需要的是一個相當複雜的檢查,需要這樣更復雜的檢查來確保沒有使用了相同名字的方法被添加到目標類以及目標類的超類中。另外,我們也可以跟蹤所有的內聯點和它們的依賴,並在類被更新時,解除對它們所做的優化。兩種方式可選擇,或是付出性能方面的代價,或是帶來更高的複雜性。

最重要的是,考慮到我們正在討論的是有着不同的內存模型和指令集的多個平臺,它們可能多多少少需要一些特定的處理,因此你給自己帶來的是一個代價過高而沒有太多投資回報的問題。

jrebel-agent

JRebel介紹

2007年,ZeroTurnaround宣佈提供一種被稱作JRebel(當時是JavaRebel)的工具,該工具可以在無需動態類加載器的情況下更新類,且只做極少的限制。不像HotSwap要依賴於IDE的集成,這一工具的工作方式是,監控磁盤上實際已編譯的.class文件,無論何時只要有文件被更新就更新類。這意味着如果願意的話,你可以把JRebel和文本編輯器、命令行的編譯器放在一起使用。當然,它也被巧妙地整合到了Eclipse、InteliJ和NetBeans中。與動態的類加載器不一樣,JRebel保留了所有現有的對象和類的標識和狀態,允許開發者繼續使用他們的應用而不會產生延遲。

如何使之生效?

對於初學者來說,JRebel工作在與HotSwap不同的一個抽象層面上。鑑於HotSwap是工作在虛擬機層面上,且依賴於JVM的內部運作,JRebel用到了JVM的兩個顯著的功能特徵——抽象的字節碼和類加載器。類加載器允許JRebel辨別出類被加載的時刻,然後實時地翻譯字節碼,用以在虛擬機和可執行代碼之間創建另一個抽象層。

也有人使用這一功能特性來提供分析器、性能監控、後續(continuation)、軟件事務性內存以及甚至是分佈式的堆。 把字節碼抽象和類加載器結合在一起,這是一種強大的組合,可被用來實現各種比類重載還要不尋常的功能。當我們越是深入地研究這一問題,我們就會看到面臨的挑戰並不僅是在類重載這件事上,而且是還要在性能和兼容性方面沒有明顯退化的情況下來做這件事情,

正如我們在Reloading Java Classes 101 一文中所做的回顧一樣,重載類存在的問題是,一旦類被載入,它就不能被卸載或是改變;但是隻要我們願意,我們就可以自由地加載新的類。爲了理解在理論上我們是如何重載類的,讓我們來研究一下Java平臺上的動態語言。具體來說,讓我們先來看一看JRudy(我們做了許多的簡化,以免對任何重要人物造成折磨)。

儘管JRuby以“類(class)”作爲其功能特性,但在運行時,其每個對象都是動態的,任何時候都可以加入新的域和方法。這意味着JRuby對象與Map沒有什麼兩樣,有着從方法名字到方法實現的映射,以及域名到其值的映射。這些方法的實現被包含在匿名的類中,在遇到方法時這些類就會被生成。如果你添加了一個方法,則所有JRuby要做的事情就是生成一個新的匿名類,該類包含了這一方法的方法體。因爲每個匿名類都有一個唯一的名稱,因此在加載該類是不會有問題的,而這樣做的結果是,應用被實時動態地更新了。

從理論上來說,由於字節碼翻譯通常是用來修改類的字節碼,因此若僅僅是爲了根據需要創建足夠多的類來履行類的功能的話,我們沒有什麼理由不能使用類中的信息。這樣的話,我們就可以使用如JRuby所做的相同轉換來把所有的Java類分割成持有者類和方法體類。不幸的是,這樣的一種做法會遭受(至少是)如下的問題:

性能。這樣的設置將意味着,每個方法調用都會遭遇重定向。我們可以做優化,但應用程序的速度將會變慢至少一個數量級,內存的使用也會扶搖直上,因爲有這麼多的類被創建。 Java的SDK類。Java SDK中的類明顯地比應用或是庫中的類更加難以處理。此外它們通常會以本地的代碼來實現,因此不能以“JRuby”的方式做轉換。然而,如果我們讓它們保持原樣的話,那麼就會引發各種的不兼容性錯誤,這些錯誤有可能是無法繞開的。 兼容性。儘管Java是一種靜態的語言,但是它包含了一些動態的特性,比如說反射和動態代理等。如果我們採用了“JRuby”式的轉換的話,這些功能特性就會失效,除非我們使用自己的類來替換掉Reflection API,而這些類知道這些要做的轉換。

因此,JRebel並沒有採用這樣的做法。相反,其使用了一種更復雜的方法,基於先進的編譯技術,留給我們一個主類和幾個匿名的支持類,這些類由JIT的轉換運行時做支持,其允許所進行的修改不會帶來任何明顯的性能或是兼容性的退化。它還

留有儘可能多完整的方法調用,這意味着JRebel把性能開銷降低到了最小,使其輕量級化。
避免了改編(instrument)Java SDK,除了少數幾個需要保持兼容性的地方外。
調整Reflection API的結果,這樣我們就能夠把這些結果中已添加/已刪除的成員正確地包含進來。這也意味着註解(Annotation)的改變對於應用來說是可見的。

除了類重載之外——還有歸檔文件

重載類是一件Java開發者已經抱怨了很久的事情,不過一旦我們解決了它之後,另外的一些問題就隨之而來了。

Java EE標準的制定並未怎麼關注開發的週轉期(Turnaround)(指的是從對代碼做修改到觀察到改變在應用中造成的影響這一過程所花費的時間)。其設想的是,所有的應用和它們的模塊都被打包到歸檔文件(JAR、WAR和EAR)中,這意味着在能夠更新應用中的任何文件之前,你需要更新歸檔文件——這通常是一個代價高昂的操作,涉及了諸如Ant或是Maven這一類的構建系統。正如我們在Reloading Java Classes 301 所做的討論那樣,可以通過使用展開式的開發和增量的IDE構建來儘量減少花銷,不過對於大型的應用來說,這種做法通常不是一個可行的選擇。

爲了解決這一問題,在JRebel 2.x中,我們爲用戶開發了一種方式來把歸檔的應用和模塊映射回到工作區中——用戶在每個應用和模塊中創建一個rebel.xml配置文件,該文件告訴JRebel在哪裏可以找到源文件。JRebel與應用服務器整合在一起,當某個類或是資源被更新時,其被從工作區中而不是從歸檔文件中讀入。

workspace-map

這一做法不僅允許類的即時更新,且允許諸如HTML、XML、JSP、CSS、.properties等之類的任何類型的資源的即時更新。Maven用戶甚至不需要創建一個rebel.xml文件,因爲Maven插件會自動地生成該文件。

除了類重載之外——還有配置和元數據

在消除週轉期的這一過程中,另一個問題變得明顯起來:現如今的應用已不僅僅是類和資源,它們還通過大量的配置和元數據綁定在一起。當配置發生改變時,改變應該被反映到那個正在運行的應用上。然而,僅把對配置文件的修改變成是可見的是不夠的,具體的框架必須要要重載配置,把改變反映到應用中才行。

conf

爲了在JRebel中支持這些類型的改變,我們開發了一個開源的API ,該API允許我們的團隊和第三方的捐獻者使用框架特有的插件來使用JRebel的功能特性,把配置中所做的改變傳播到框架中。例如,我們支持動態實時地在Spring中添加bean和依賴,以及支持在其他框架中所做的各種各樣的改變。

結論

本文總結了在未使用動態類加載器情況下的各種重載Java類的方法。我們還討論了導致HotSwap侷限性的原因,揭示了JRebel幕後的工作方式,以及討論了在解決類重載問題時出現的其他問題。

原文地址:http://article.yeeyan.org/view/213582/186226

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章