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

點擊上方 IT牧場 ,選擇 置頂或者星標技術乾貨每日送達!


什麼是classloader



classloader顧名思義,即是類加載。虛擬機把描述類的數據從class字節碼文件加載到內存,並對數據進行檢驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。瞭解java的類加載機制,可以快速解決運行時的各種加載問題並快速定位其背後的本質原因,也是解決疑難雜症的利器。因此學好類加載原理也至關重要。


  classloader的加載過程


類從被加載到虛擬機內存到被卸載,整個完整的生命週期包括:類加載、驗證、準備、解析、初始化、使用和卸載七個階段。其中驗證,準備,解析三個部分統稱爲連接。接下來我們可以詳細瞭解下類加載的各個過程。



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


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


  classloader雙親委託機制


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



整個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)通常是我們爲了某些特殊目的實現的自定義加載器,後面我們得會詳細介紹到它的作用以及使用場景。


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




classloader的應用場景



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


  依賴衝突


做過多人協同開發的大型項目的同學可能深有感觸。基於maven的pom進制可以方便的進行依賴管理,但是由於maven依賴的傳遞性,會導致我們的依賴錯綜複雜,這樣就會導致引入類衝突的問題。最典型的就是NoSuchMethodException異常了。


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



某個業務引用了消息中間件(例如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實現依賴隔離的基礎。



可能到這裏,你又會有新的疑惑,根據雙親委託模型,App Classloader分別繼承了Custom Classloader.那麼業務包中的fastjson的class在加載的時候,會先委託到Custom ClassLoader。這樣不就會導致自身依賴的fastjson版本被忽略嗎?確實如此,所以潘多拉又是如何做的呢?


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


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



  熱加載


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


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


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



如上圖所示,通常一個項目的代碼由以上四部分組成,即基礎類、擴展類、二方包/三方包、以及我們自己編寫的業務代碼組成。上面的一排是我們通常的類加載結構,其中業務代碼和二方包/三方包是由應用加載器加載的。而實際開發和調試的過程中,主要變化的是業務代碼,並且業務代碼相對二方包/三方包的內容來說會更少一些。因此我們可以將業務代碼單獨通過一個自定義的加載器Custom Classloader來進行加載,當監控發現業務代碼發生改變後,我們重新加載啓動,老的業務代碼的相關類則由虛擬機的垃圾回收機制來自動回收。其工程流程大概如下。有興趣的同學可以去看下源碼,會更加清楚。



RestartClassLoader爲自定義的類加載器,其核心是loadClass的加載方式,我們發現其通過修改了雙親委託機制,默認優先從自己加載,如果自己沒有加載到,從從parent進行加載。這樣保證了業務代碼可以優先被RestartClassLoader加載。進而通過重新加載RestartClassLoader即可完成應用代碼部分的重新加載。



  部署


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



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


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


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


  加密保護


衆所週期,基於java開發編譯產生的jar包是由.class字節碼組成,由於字節碼的文件格式是有明確規範的。因此對於字節碼進行反編譯,就很容易知道其源碼實現了。因此大致會存在如下兩個方面的訴求。例如在服務端,我們向別人提供三方包實現的時候,不希望別人知道核心代碼實現,我們可以考慮對jar包進行加密,在客戶端則會比較普遍,那就是我們打包好的apk的安裝包,不希望被人家反編譯而被人家翻個底朝天,我們也可以對apk進行加密。


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



我們可以貼一下簡單的實現方案:



這樣整個jar包的安全性就有一定程度的提高,至於更高安全的保障則取決於加密算法的安全性了以及如何保障加密算法的密鑰不被泄露的問題了。這有種套娃的感覺,所謂安全基本都是相對的。並且這些方法也不是絕對的,例如可以通過對classloader進行插碼,對解密後的class文件進行存儲;另外大多數JVM本身並不安全,還可以修改JVM,從ClassLoader之外獲取解密後的代碼並保存到磁盤,從而繞過上述加密所做的一切工作,當然這些操作的成本就比單純的class反編譯就高很多了。所以說安全保障只要做到使對方破解的成本高於收益即是安全,所以一定程度的安全性,足以減少很多低成本的攻擊了。



總結



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


作者| 金雅博(行澤)
編輯| 橙子君
出品| 阿里巴巴新零售淘系技術

乾貨分享

最近將個人學習筆記整理成冊,使用PDF分享。關注我,回覆如下代碼,即可獲得百度盤地址,無套路領取!

001:《Java併發與高併發解決方案》學習筆記;002:《深入JVM內核——原理、診斷與優化》學習筆記;003:《Java面試寶典》004:《Docker開源書》005:《Kubernetes開源書》006:《DDD速成(領域驅動設計速成)》007:全部008:加技術羣討論

近期熱文

LinkedBlockingQueue vs ConcurrentLinkedQueue解讀Java 8 中爲併發而生的 ConcurrentHashMapRedis性能監控指標彙總最全的DevOps工具集合,再也不怕選型了!微服務架構下,解決數據庫跨庫查詢的一些思路聊聊大廠面試官必問的 MySQL 鎖機制

關注我

喜歡就點個"在看"唄^_^


本文分享自微信公衆號 - IT牧場(itmuch_com)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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