天道不一定酬所有勤
但是,天道只酬勤

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中時,對象是由內存中的結構來表示的,結構占據了某個特定大?。ㄋ挠蚣由显獢祿┑倪B續的內存區域。為了添加一個域,我們需要調整結構的大小,但因為臨近的區域可能已被占用,我們就需要把整個結構重新分配到一個不同的區域中,這一區域中有足夠可用的空間來把它填寫進來?,F在,由于我們實際上是更新了一個類(并不僅是某個對象),所以我們不得不對該類的每一個對象都做這樣的一件事。

這本身并不難實現——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

(全文完) 歡迎關注『Java之道』微信公眾號
贊(6)
如未加特殊說明,此網站文章均為原創,轉載必須注明出處。HollisChuang's Blog » HotSwap和JRebel原理
分享到: 更多 (0)

評論 4

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
  1. #1

    yoyy點xyz 狠友福利

    松舞龍心2年前 (2018-05-01)回復
  2. #2

    kokk點xyz 狠ff友ff福ff利

    狂想曲2年前 (2018-06-07)回復
  3. #3

    yoyy點xyz 狠ff友ff福ff利

    貝多芬的彈唱2年前 (2018-06-08)回復
  4. #4

    神作啊

    涼生癡心妄想2年前 (2018-06-26)回復

HollisChuang's Blog

聯系我關于我
网上设计赚钱的网站有哪些