天天看點

Java類加載器 — classloader 的原理及應用總結

什麼是classloader

classloader顧名思義,即是類加載。虛拟機把描述類的資料從class位元組碼檔案加載到記憶體,并對資料進行檢驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型,這就是虛拟機的類加載機制。了解java的類加載機制,可以快速解決運作時的各種加載問題并快速定位其背後的本質原因,也是解決疑難雜症的利器。是以學好類加載原理也至關重要。

▐ classloader的加載過程

類從被加載到虛拟機記憶體到被解除安裝,整個完整的生命周期包括:類加載、驗證、準備、解析、初始化、使用和解除安裝七個階段。其中驗證,準備,解析三個部分統稱為連接配接。接下來我們可以詳細了解下類加載的各個過程。

Java類加載器 — classloader 的原理及應用總結

classloader的整個加載過程還是非常複雜的,具體的細節可以參考《深入了解java虛拟機》進行深入了解。為了友善記憶,我們可以使用一句話來表達其加載的整個過程,“家宴準備了西式菜”,即家(加載)宴(驗證)準備(準備)了西(解析)式(初始化)菜。保證你以後能夠很快的想起來。

雖然classloader的加載過程有複雜的5步,但事實上除了加載之外的四步,其它都是由JVM虛拟機控制的,我們除了适應它的規範進行開發外,能夠幹預的空間并不多。而加載則是我們控制classloader實作特殊目的最重要的手段了。也是接下來我們介紹的重點了。

▐ classloader雙親委托機制

classloader的雙親委托機制是指多個類加載器之間存在父子關系的時候,某個class類具體由哪個加載器進行加載的問題。其具體的過程表現為:當一個類加載的過程中,它首先不會去加載,而是委托給自己的父類去加載,父類又委托給自己的父類。是以所有的類加載都會委托給頂層的父類,即Bootstrap Classloader進行加載,然後父類自己無法完成這個加載請求,子加載器才會嘗試自己去加載。使用雙親委派模型,Java類随着它的加載器一起具備了一種帶有優先級的層次關系,通過這種層次模型,可以避免類的重複加載,也可以避免核心類被不同的類加載器加載到記憶體中造成沖突和混亂,進而保證了Java核心庫的安全。

Java類加載器 — classloader 的原理及應用總結

整個java虛拟機的類加載層次關系如上圖所示,啟動類加載器(Bootstrap Classloader)負責将<JAVA_HOME>/lib目錄下并且被虛拟機識别的類庫加載到虛拟機記憶體中。我們常用基礎庫,例如java.util.,java.io.,java.lang.**等等都是由根加載器加載。

擴充類加載器(Extention Classloader)負責加載JVM擴充類,比如swing系列、内置的js引擎、xml解析器等,這些類庫以javax開頭,它們的jar包位于<JAVA_HOME>/lib/ext目錄中。

應用程式加載器(Application Classloader)也叫系統類加載器,它負責加載使用者路徑(ClassPath)上所指定的類庫。我們自己編寫的代碼以及使用的第三方的jar包都是由它來加載的。

自定義加載器(Custom Classloader)通常是我們為了某些特殊目的實作的自定義加載器,後面我們得會詳細介紹到它的作用以及使用場景。

雙親委托機制看起來比較複雜,但是其本身的核心代碼邏輯卻是非常的清晰簡單,我們着重抽取了類加載的雙親委托的核心代碼如下,不過二十行左右。

Java類加載器 — classloader 的原理及應用總結

classloader的應用場景

類加載器是java語言的一項創新,也是java語言流行的重要原因這一。通過靈活定義classloader的加載機制,我們可以完成很多事情,例如解決類沖突問題,實作熱加載以及熱部署,甚至可以實作jar包的加密保護。接下來,我們會針對這些特殊場景進行逐一介紹。

▐ 依賴沖突

做過多人協同開發的大型項目的同學可能深有感觸。基于maven的pom進制可以友善的進行依賴管理,但是由于maven依賴的傳遞性,會導緻我們的依賴錯綜複雜,這樣就會導緻引入類沖突的問題。最典型的就是NoSuchMethodError錯誤。

在阿裡平時的項目開發中是否也會遇到類似的問題嗎,答案是肯定的。例如阿裡内部也很多成熟的中間件,由不同的中間件團隊來負責。那麼當一個項目引入不同的中間件的時候,該如何避免依賴沖突的問題呢?首先我們用一個非常簡單的場景來描述為什麼會出現類沖突的問題。

Java類加載器 — classloader 的原理及應用總結

某個業務引用了消息中間件(例如metaq)和微服務中間件(例如dubbo),這兩個中間件也同時引用了fastjson-2.0和fastjson-3.0版本,而業務自己本身也引用了fastjson-1.0版本。這三個版本表現不同之處在于classA類中方法數目不相同,我們根據maven依賴處理的機制,引用路徑最短的fastjson-1.0會真正作為應用最終的依賴,其它兩個版本的fastjson則會被忽略,那麼中間件在調用method2()方法的時候,則會抛出方法找不到異常。或許你會說,将所有依賴fastjson的版本都更新到3.0不是就能解解決問題嗎?确實這樣能夠解決問題,但是在實際操作中不太現實,首先,中間件團隊和業務團隊之間并不是一個團隊,并不能做到高效協同,其次是中間件的穩定性是需要保障的,不可能因為包沖突問題,就更新版本,更何況一個中間件依賴的包可能有上百個,如果純粹依賴包更新來解決,不僅穩定性難以保障,排包耗費的時間恐怕就讓人窒息了。

那如何解決包沖突的問題呢?答案就是pandora(潘多拉),通過自定義類加載器,為每個中間件自定義一個加載器,這些加載器之間的關系是平行的,彼此沒有依賴關系。這樣每個中間件的classloader就可以加載各自版本的fastjson。因為一個類的全限定名以及加載該類的加載器兩者共同形成了這個類在JVM中的惟一辨別,這也是阿裡pandora實作依賴隔離的基礎。

Java類加載器 — classloader 的原理及應用總結
可能到這裡,你又會有新的疑惑,根據雙親委托模型,App Classloader分别繼承了Custom Classloader.那麼業務包中的fastjson的class在加載的時候,會先委托到Custom ClassLoader。這樣不就會導緻自身依賴的fastjson版本被忽略嗎?确實如此,是以潘多拉又是如何做的呢?
Java類加載器 — classloader 的原理及應用總結

首先每個中間件對應的ModuleClassLoader在加載中間對應的class檔案的同時,根據中間件配置的export.index負責将要需要透出的class(主要是中間件api接口的相關類)索引到exportedClassHashMap中,然後應用程式的類加載器會持有這個exportedClassHashMap,是以應用程式代碼在loadClass的時候,會優先判斷exportedClassHashMap是否存在目前類,如果存在,則直接傳回,如果不存在,則再使用傳統的雙親委托機制來進行類加載。這樣中間件MoudleClassloader不僅實作了中間件的加載,也實作了中間件關鍵服務類的透出。

我們可以大概看下應用程式類加載的過程:

Java類加載器 — classloader 的原理及應用總結

▐ 熱加載

在開發項目的時候,我們需要頻繁的重新開機應用進行程式調試,但是java項目的啟動少則幾十秒,多則幾分鐘。如此慢的啟動速度極大地影響了程式開發的效率,那是否可以快速的進行啟動,進而能夠快速的進行開發驗證呢?答案也是肯定的,通過classloader我們可以完成對變更内容的加載,然後快速的啟動。

常用的熱加載方案有好幾個,接下來我們介紹下spring官方推薦的熱加載方案,即spring boot devtools。

首先我們需要思考下,為什麼重新啟動一個應用會比較慢,那是因為在啟動應用的時候,JVM虛拟機需要将所有的應用程式重新裝載到整個虛拟機。可想而知,一個複雜的應用程式所包含的jar包可能有上百兆,每次微小的改動都是全量加載,那自然是很慢了。那麼我們是否可以做到,當我們修改了某個檔案後,在JVM中替換到這個檔案相關的部分而不全量的重新加載呢?而spring boot devtools正是基于這個思路進行處理的。

Java類加載器 — classloader 的原理及應用總結
如上圖所示,通常一個項目的代碼由以上四部分組成,即基礎類、擴充類、二方包/三方包、以及我們自己編寫的業務代碼組成。上面的一排是我們通常的類加載結構,其中業務代碼和二方包/三方包是由應用加載器加載的。而實際開發和調試的過程中,主要變化的是業務代碼,并且業務代碼相對二方包/三方包的内容來說會更少一些。是以我們可以将業務代碼單獨通過一個自定義的加載器Custom Classloader來進行加載,當監控發現業務代碼發生改變後,我們重新加載啟動,老的業務代碼的相關類則由虛拟機的垃圾回收機制來自動回收。其工程流程大概如下。有興趣的同學可以去看下源碼,會更加清楚。
Java類加載器 — classloader 的原理及應用總結
RestartClassLoader為自定義的類加載器,其核心是loadClass的加載方式,我們發現其通過修改了雙親委托機制,預設優先從自己加載,如果自己沒有加載到,從從parent進行加載。這樣保證了業務代碼可以優先被RestartClassLoader加載。進而通過重新加載RestartClassLoader即可完成應用代碼部分的重新加載。
Java類加載器 — classloader 的原理及應用總結

▐ 熱部署

熱部署本質其實與熱加載并沒有太大的差別,通常我們說熱加載是指在開發環境中進行的classloader加載,而熱部署則更多是指線上上環境使用classloader的加載機制完成業務的部署。是以這二者使用的技術并沒有本質的差別。那熱部署除了與熱加載具有釋出更快之外,還有更多的更大的優勢就是具有更細的釋出粒度。我們可以想像以下的一個業務場景。

Java類加載器 — classloader 的原理及應用總結

假設某個營銷投放平台涉及到4個業務方的開發,需要對會場業務進行投放。而這四個業務方的代碼全部都在一個應用裡面。是以某個業務方有代碼變更則需要對整個應用進行釋出,同時其它業務方也需要跟着回歸。是以每個微小的發動,則需要走整個應用的全量釋出。這種方式帶來的穩定性風險估且不說,整個釋出疊代的效率也可想而知了。這在整個網際網路裡,時間和效率就是金錢的理念下,顯然是無法接受的。

那麼我們完全可以通過類加載機制,将每個業務方通過一個classloader來加載。基于類的隔離機制,可以保障各個業務方的代碼不會互相影響,同時也可以做到各個業務方進行獨立的釋出。其實在移動用戶端,每個應用子產品也可以基于類加載,實作插件化釋出。本質上也是一個原理。

在阿裡内部像阿拉丁投放平台,以及crossbow容器化平台,本質都是使用classloader的熱加載技術,實作業務細粒度的開發部署以及多應用的合并部署。

▐ 加密保護

衆所周期,基于java開發編譯産生的jar包是由.class位元組碼組成,由于位元組碼的檔案格式是有明确規範的。是以對于位元組碼進行反編譯,就很容易知道其源碼實作了。是以大緻會存在如下兩個方面的訴求。例如在服務端,我們向别人提供三方包實作的時候,不希望别人知道核心代碼實作,我們可以考慮對jar包進行加密,在用戶端則會比較普遍,那就是我們打包好的apk的安裝包,不希望被人家反編譯而被人家翻個底朝天,我們也可以對apk進行加密。

jar包加密的本質,還是對位元組碼檔案進行操作。但是JVM虛拟機加載class的規範是統一的,是以我們在最終加載class檔案的時候,還是需要滿足其class檔案的格式規範,否則虛拟機是不能正常加載的。是以我們可以在打包的時候對class進行正向的加密操作,然後,在加載class檔案之前通過自定義classloader先進行反向的解密操作,然後再按照标準的class檔案标準進行加載,這樣就完成了class檔案正常的加載。是以這個加密的jar包隻有能夠實作解密方法的classloader才能正常加載。

Java類加載器 — classloader 的原理及應用總結
我們可以貼一下簡單的實作方案:
Java類加載器 — classloader 的原理及應用總結
這樣整個jar包的安全性就有一定程度的提高,至于更高安全的保障則取決于加密算法的安全性了以及如何保障加密算法的密鑰不被洩露的問題了。這有種套娃的感覺,所謂安全基本都是相對的。并且這些方法也不是絕對的,例如可以通過對classloader進行插碼,對解密後的class檔案進行存儲;另外大多數JVM本身并不安全,還可以修改JVM,從ClassLoader之外擷取解密後的代碼并儲存到磁盤,進而繞過上述加密所做的一切工作,當然這些操作的成本就比單純的class反編譯就高很多了。是以說安全保障隻要做到使對方破解的成本高于收益即是安全,是以一定程度的安全性,足以減少很多低成本的攻擊了。

總結

本文對classloader的加載過程和加載原理進行了介紹,并結合類加載機制的特征,介紹了其相應的使用場景。由于篇幅限制,并沒有對每種場景的具體實作細節進行介紹,而隻是闡述了其基本實作思路。或許大家覺得classloader的應用有些複雜,但事實上隻要大家對class從哪裡加載,搞清楚loadClass的機制,就已經成功了一大半。正所謂萬變不離其宗,抓住了本質,其它問題也就迎刃而解了。