漫談JVM熱載入技術(一)—目前常見的解決方案

遮山發表於2016-11-28

目前的Hot Reload方案

目前一般是容器(Web Container/Framework)才有能力做到熱載入。因為通過自定義的ClassLoader例項來管理(bean/page/controller/configuration),如果這些檔案有變化,立即建立一個新的ClassLoader例項來載入新的資原始檔。例如:tomcat/jetty/Resin/…/SEAM/Grails

1、Hot deploy

應該稱之為:熱部署。熱部署並不神祕,最暴力的熱部署是自動重啟當前應用的JVM。常見的熱部署指是在不影響當前JVM中其它應用的前提下只對需要重新部署的程式進行更新。

基本上目前所有的應用伺服器都支援熱部署。但是如果應用太大,熱部署的耗時是按照分鐘來計:重新初始化各種配置、預熱快取、資料校驗,可能會出現記憶體洩露的問題:http://zeroturnaround.com/rebellabs/rjc201/

以tomcat的hot deploy為例: class檔案被修改過,那麼Tomcat會先解除安裝這個應用(Context),然後重新載入這個應用,其中關鍵就在於自定義ClassLoader的使用方式。

  • 首先$CATALINA_HOME/conf/context.xml 中配置:

    <Context reloadable="true">
    ...
    </Context>
  • tomcat啟動,載入當前context:StandContext.start(),context啟動成功後,會由ContainerBase中啟動一個名為ContainerBackgroundProcessor的執行緒來監控是否有資原始檔被修改,如果被修改再呼叫StandContext.reload()方法(先stop(),然後start())。整個流程就像陰陽魚,生生不息。
    tomcat_seq
  • StandContext.start()方法會呼叫WebappLoader.start(),為當前context建立一個專用WebappClassLoader:也就說每次context的載入都會有一個專屬的WebappClassLoader。原因在於JVM Classloader體系的限制:web容器即使能夠部分突破雙親委託載入規則的限制來載入某些class檔案(原因1),但是依然無法突破一個ClassLoader不能重複載入某個class的限制(原因2),如果要保證修改後的class檔案被重新載入,則需要重新建立一個ClassLoader例項來載入所有的class檔案和資原始檔,同時當前context中已經由之前ClassLoader例項載入的資源必須丟棄,否則會出現ClassCastExeption異常。

class_reload

  • tomcat的WebappClassLoader重寫了findLoadedClass0()、findClass()方法,主要是在更改的邏輯在於:自己優先載入class檔案,然後根據情況決定是否由父類載入;自定義快取機制來快取自己已經載入的class資源。

    • 原因1:ClassLoader.loadClass(…) 是ClassLoader的入口點。當一個類沒有指明用什麼載入器載入的時候,JVM預設採用AppClassLoader載入器載入沒有載入過的class,呼叫的方法的入口就是loadClass(…)。如果一個class被自定義的ClassLoader載入,那麼JVM也會呼叫這個自定義的ClassLoader.loadClass(…)方法來載入class內部引用的一些別的class檔案。過載這個方法,能實現自定義載入class的方式,拋棄雙親委託機制,但是即使不採用雙親委託機制,比如java.lang包中的相關類還是不能自定義一個同名的類來代替,主要因為JVM解析、驗證class的時候,會進行相關判斷。
    • 原因2: 系統自帶的ClassLoader,預設載入程式的是AppClassLoader,ClassLoader載入一個class,最終呼叫的是defineClass(…)方法,這時候就在想是否可以重複呼叫defineClass(…)方法載入同一個類(或者修改過),最後發現呼叫多次的話會有相關錯誤:
java.lang.LinkageError 
attempted duplicate class definition

所以一個class被一個ClassLoader例項載入過的話,就不能再被這個ClassLoader例項再次載入(這裡的載入指的是,呼叫了defileClass(…)放方法,重新載入位元組碼、解析、驗證)。而系統預設的AppClassLoader載入器,他們內部會快取載入過的class,重新載入的話,就直接取快取。所與對於熱載入的話,只能重新建立一個ClassLoader,然後再去載入已經被載入過的class檔案。

注:tomcat版本為6.0.x,http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk
資原始檔每次的修改都會造成web容器的hot deploy,如果應用比較大,整個過程會很耗時。原因1、原因2,轉自:http://www.cnblogs.com/balaamwe/archive/2013/05/13/3076086.html

2、HotSwap

從JDK1.4提供的技術,執行開發人員在debug過程中能夠立即過載修改後的class。所有的IDE都支援這個特性(Intellij IDEA,Eclipse,NetBeans)。如果debug應用,並且修改了某些class,jvm會立即載入修改後的class。同樣,這個技術也有限制:只允許修改方法體,不允許增加新的class、不允許新增欄位、不允許新增方法、不允許修改方法簽名、不允許。。。
詳情請參考:

java.lang.instrument.Instrumentation.redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;

Play1 framework基於這個方法和hot deploy方式也實現了自己的熱載入機制。這2種方式組合在一起能夠避免僅僅是方法體被修改後重新載入context的問題,減少重新載入的次數。
Play1的熱載入整個流程比較簡潔,核心在於Eclipse Java Compile(ECJ)和自定義的ApplicationClassloader。
play_seq

Play1首先通過ECJ來編譯java檔案生成class檔案。在DEV模式下,由ApplicationClassloader負責校驗java檔案和資原始檔是否被修改過,然後決定是否重新載入class檔案,流程如下:
play_act

在Play context重啟的時候會重新建立一個新的ApplicationClassLoader例項來載入所有的資原始檔、class檔案。之前的ClassLoader例項及相關被載入的資源都被丟棄有jvm gc回收。

Play1通過Instrumentation.redefineClasses方法一定程度上減少class檔案修改導致的context重新載入問題,但是在實際開發場景中意義不大:class檔案和資原始檔經常有較大的變化;熱載入後類的靜態屬性不能初始化;不支援spring、ibatis等常見框架。。。

JRebel可以當做HotSwap的增強版本,允許修改class結構:新增方法、欄位、構造器、註解、新增class、修改配置檔案。

3、OSGi

OSGi是Java模組化執行容器。根據個人的理解,可以把OSGi當做一個獨立的Runtime,裡面暴露一些資源介面,通過不同的classloader,可以提供不同版本、但是packageName.className完全相同的資源。畢大師《OSGi原理與最佳實踐》這本中介紹了OSGi標準的各個實現框架以及OSGi的資源管理、熱載入的原理。

將應用及其依賴jar劃分到不同的module中,每次可以只發布更新某些module。這種釋出和普通web容器的hot deploy類似,只是把應用切分為bundle,粒度更細,由OSGi容器來裝載、解除安裝目標bundle,優點在於每次更新的module的變動量小於更新整個應用。OSGi框架意味著複雜繁瑣的ClassLoader結構和載入機制,熱載入機制同樣也是通過不同ClassLoader例項的來實現。。。

基於ClassLoader例項的方案總結

這種方案的有點在於實現簡單。缺點在於因為Classloader轉換,會導致一些因為ClassLoader例項變化帶來的ClassCastException,只能對被容器維護的程式碼有效果,普通的應用就很麻煩了。所以為了解決這些問題,ZeroTurnaround推出了JRebel。

JRebel

JRebel號稱在類載入過程中,從位元組碼層面上解決hot reload的問題。

JRebel how to work

在Classloader級別上整合到JVM上,JRebel並沒有在自定義Classloader,它只是很暴力的修改了JVM中Classloader的一些方法體邏輯,通過asm和jdk instrumentation的機制把ClassLoader的部分方法(包括native方法)的邏輯重寫,使之能夠管理過載的class檔案。
JRebel能夠對應用中的任何class起作用,也不會導致任何和Classloader相關的問題。

當一個class需要被載入,JRebel會在classpath或者rebel.xml配置指定的路徑中試圖查詢相應的class檔案。如果找到class檔案,JRebel通過agent機制instrument這個class,並且維護class和class檔案的關聯關係。當應用中已經載入的class對應的class檔案的修改時間變動後,擴充套件的Classloader就會被觸發來載入新的class(Classloader並不會主動載入,而是在每次使用這個class的時候,check timestamp決定是否要載入class檔案)。

JRebel同樣能夠監控rebel.xml上配置的JARs中的class檔案。

重新載入配置檔案

僅僅重新載入Java class是不能滿足開發需求的。應用程式是由程式碼和配置檔案(XML、Properties、Annotation等)組成的。JRebel能夠重新載入修改後的配置檔案。

JRebel只使用了Instrumentation API(http://java.sun.com/javase/6/docs/api/java/lang/instrument/package-summary.html
Instrumentation API在JDK5中引入的,支援執行過程中有限制的修改Java class。杯具的是這個限制就是要求只能修改方法體(和HotSwap一樣)。JRebel使用了instrumentation來處理classloader和一些基礎類,但是在實際的reload過程中沒有任何用處。

深入理解JRebel

???
404
!!!
很抱歉,這是個收費的產品,原始碼已經被混淆了,很難了解整個hot reload的呼叫流程。JRebel解決了class熱載入的問題,但是帶來了新的問題:沒有原始碼、商用付費(當然,如果你使用河蟹版,就忽略這些新問題)

so,為了解決這個問題,Hotcode2應時而生

Hotcode2

《漫談JVM熱載入技術(二)—Hotcode2 reload機制》

參考

http://www.cnblogs.com/balaamwe/archive/2013/05/13/3076086.html
http://www.infoq.com/cn/articles/code-generation-with-osgi
https://www.ibm.com/developerworks/cn/java/j-lo-osgi/
http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
http://zeroturnaround.com/software/jrebel/learn/faq/
http://zeroturnaround.com/rebellabs/rjc201/
http://manuals.zeroturnaround.com/jrebel/standalone/config.html#maven-rebel-xml
http://zeroturnaround.com/rebellabs/how-my-new-friend-byte-buddy-enables-annotation-driven-java-runtime-code-generation/


相關文章