在JVM之下–类加载器
在許多開發(fā)人員中,類加載器是Java語言的底層,并且經(jīng)常被忽略。 在ZeroTurnaround上 ,我們的開發(fā)人員必須生活,呼吸,飲食,喝酒,并且?guī)缀跖c類加載器保持親密關(guān)系,才能生產(chǎn)JRebel技術(shù),該技術(shù)在類加載器級(jí)別進(jìn)行交互以提供實(shí)時(shí)運(yùn)行時(shí)類重裝,從而避免了冗長(zhǎng)的重建/重新包裝/重新部署周期。
以下是我們從類加載器中學(xué)到的一些知識(shí),其中包括一些調(diào)試技巧,這些技巧將有望為您節(jié)省時(shí)間和將來的總服務(wù)臺(tái)。
一個(gè)類加載器只是一個(gè)普通的java對(duì)象
是的,這并不聰明,除了JVM中的系統(tǒng)類加載器之外,類加載器只是一個(gè)Java對(duì)象! 這是一個(gè)抽象類ClassLoader,可以由您創(chuàng)建的類實(shí)現(xiàn)。 這是API:
public abstract class ClassLoader {public Class loadClass(String name);protected Class defineClass(byte[] b);public URL getResource(String name);public Enumeration getResources(String name);public ClassLoader getParent()}看起來很簡(jiǎn)單,對(duì)吧? 讓我們逐個(gè)方法看一下。 中心方法是loadClass,它僅使用String類名,然后返回實(shí)際的Class對(duì)象。 如果您以前使用過類加載器,則此方法可能是最熟悉的方法,因?yàn)樗侨粘>幋a中使用最多的方法。 defineClass是JVM中的最終方法,該方法從網(wǎng)絡(luò)上的文件或位置獲取字節(jié)數(shù)組,并產(chǎn)生相同的結(jié)果(即Class對(duì)象)。
類加載器還可以從類路徑中找到資源。 它的工作方式與loadClass方法類似。 有兩種方法,getResource和getResources,它們返回一個(gè)URL或URL的枚舉,這些URL或URL的枚舉指向資源,該資源表示傳遞給方法的名稱。
每個(gè)類加載器都有一個(gè)父級(jí)。 getParent返回與Java繼承無關(guān)的classloader父類,而是一個(gè)鏈表樣式的連接。 稍后我們將對(duì)此進(jìn)行更深入的研究。
類加載器是惰性的,因此僅在運(yùn)行時(shí)請(qǐng)求類時(shí)才加載類。 類是由調(diào)用該類的資源加載的,因此,在運(yùn)行時(shí),一個(gè)類可以由多個(gè)類加載器加載,具體取決于從何處引用它們,以及哪個(gè)類加載器加載了引用了這些類的類…哎呀,我cross地了! 讓我們看一些代碼。
public class A {public void doSmth() {B b = new B();b.doSmthElse();}}在這里,我們有一個(gè)類A在其方法范圍內(nèi)調(diào)用類B的構(gòu)造函數(shù)。 在幕后這是正在發(fā)生的事情
A.class.getClassLoader().loadClass(“B”);最初加載類A的類加載器被調(diào)用以加載類B。
類加載器是分層的,但是像孩子一樣,他們并不總是問父母
每個(gè)類加載器都有一個(gè)父類加載器。 當(dāng)一個(gè)類加載器被要求提供一個(gè)類時(shí),它通常會(huì)直接轉(zhuǎn)到父類加載器,首先調(diào)用loadClass,而后者又會(huì)詢問它的父類,依此類推。 如果要求具有相同父級(jí)的兩個(gè)類加載器加載同一類,則父級(jí)將只執(zhí)行一次。 當(dāng)兩個(gè)類加載器分別加載同一個(gè)類時(shí),這將非常麻煩,因?yàn)檫@可能會(huì)導(dǎo)致問題,我們將在后面討論。
當(dāng)設(shè)計(jì)JEE規(guī)范時(shí),Web類加載器被設(shè)計(jì)為以相反的方式工作-很棒。 讓我們看一下下圖作為示例。
模塊WAR1有自己的類加載器,并且更喜歡自行加載類,而不是委托給其父級(jí)(由App1.ear定義的類加載器)。 這意味著不同的WAR模塊(例如WAR1和WAR2)無法看到彼此的類。 App1.ear模塊具有自己的類加載器,并且是WAR1和WAR2類加載器的父級(jí)。 當(dāng)WAR1和WAR2類加載器需要在層次結(jié)構(gòu)中委派請(qǐng)求時(shí),即WAR類加載器范圍之外需要一個(gè)類時(shí),它們將使用App1.ear類加載器。 實(shí)際上,WAR類會(huì)覆蓋同時(shí)存在的EAR類。 最后,EAR類加載器的父級(jí)是容器類加載器。 EAR類加載器會(huì)將請(qǐng)求委派給容器類加載器,但它的執(zhí)行方式與WAR類加載器不同,因?yàn)镋AR類加載器實(shí)際上更喜歡委托而不是本地類。 如您所見,這變得非常繁瑣,并且與普通的JSE類加載行為不同。
平面類路徑
我們討論了系統(tǒng)類加載器如何通過類路徑查找已請(qǐng)求的類。 該類路徑可能包含目錄或JAR文件,查找它們的順序?qū)嶋H上取決于您使用的JVM。 您在類路徑上可能需要該類的多個(gè)副本或版本,但是您將始終在類路徑上找到該類的第一個(gè)實(shí)例。 本質(zhì)上,這只是資源列表,這就是為什么將其稱為扁平資源。 結(jié)果,在查找資源時(shí),遍歷類路徑列表通常會(huì)比較慢。
當(dāng)使用相同類路徑的應(yīng)用程序想要使用類的不同版本時(shí),可能會(huì)發(fā)生問題,讓我們以Hibernate為例。 當(dāng)類路徑上存在兩個(gè)版本的Hibernate JAR時(shí),一個(gè)版本不能比一個(gè)應(yīng)用程序的版本路徑在另一個(gè)應(yīng)用程序的類路徑上更高,這意味著兩個(gè)版本都必須使用相同的版本。 解決此問題的一種方法是使用所有必需的庫使應(yīng)用程序(WAR)膨脹,以便它們使用其本地資源,但這會(huì)導(dǎo)致難以維護(hù)的大型應(yīng)用程序。 歡迎來到JAR地獄! OSGi在此提供了一種解決方案,因?yàn)樗试S對(duì)JAR文件或捆綁軟件進(jìn)行版本控制,從而形成一種機(jī)制,允許連接到特定版本的JAR文件,從而避免了平坦的類路徑問題。
如何調(diào)試類加載錯(cuò)誤?
NoClassDefFoundError / ClassNotFoundException / ClassNoDefFoundException?
因此,您遇到了上述錯(cuò)誤/異常。 好吧,這個(gè)班級(jí)真的存在嗎? 不要在IDE中尋找麻煩,因?yàn)樵谀莾壕幾g類是必須的,因?yàn)樗仨氃谀抢?#xff0c;否則您將獲得編譯時(shí)異常。 這是一個(gè)運(yùn)行時(shí)異常,因此在運(yùn)行時(shí)我們要查找它說我們?nèi)鄙俚念悺悄鷱哪睦镩_始呢? 考慮下面的代碼…
Arrays.toString((((URLClassLoader) Test.class.getClassLoader()).getURLs()));此代碼返回Test正在使用的類加載器的類路徑上所有jar和目錄的數(shù)組列表。 現(xiàn)在,我們可以看到神秘類應(yīng)該存在的JAR或位置實(shí)際上在類路徑上。 如果不存在,請(qǐng)?zhí)砑?#xff01; 如果確實(shí)存在,請(qǐng)檢查JAR /目錄,以確保您的類確實(shí)存在于該位置,并在缺少該類時(shí)添加它。 這是導(dǎo)致此錯(cuò)誤情況的兩個(gè)典型問題。
NoSuchMethodError / NoSuchFieldError / AbstractMethodError / IllegalAccessError嗎?
現(xiàn)在變得越來越有趣了! 這些都是IncompatibleClassChangeError的所有子類。 我們知道類加載器已經(jīng)找到了想要的類(按名稱),但是顯然它沒有找到正確的版本。
在這里,我們有一個(gè)稱為Test的類,它正在調(diào)用另一個(gè)類Util,但是BANG –我們遇到了異常! 讓我們看一下要調(diào)試的下一個(gè)代碼片段:
我們?cè)陬怲est的類加載器上調(diào)用getResource。 這將向我們返回Util資源的URL。 請(qǐng)注意,我們已替換了“。” 帶有“ /”,并在字符串末尾添加“ .class”。 這會(huì)將我們正在尋找的類的包和類名(從類加載器的角度來看)更改為文件系統(tǒng)上的目錄結(jié)構(gòu)和文件名-簡(jiǎn)潔。 這將向我們顯示我們已加載的確切類,并且可以確保它是正確的版本。 我們可以在命令提示符下在類上使用javap -private來查看字節(jié)碼并檢查實(shí)際存在的方法和字段。 您可以輕松地查看該類的結(jié)構(gòu),并驗(yàn)證是您還是瘋了的Java運(yùn)行時(shí)! 相信我,在一個(gè)或另一個(gè)階段,您都會(huì)同時(shí)問這兩個(gè)問題,幾乎每次都是您!
LinkageError / ClassCastException / IllegalAccessError
如果兩個(gè)不同的類加載器加載同一個(gè)類,并且它們嘗試進(jìn)行交互,則可能會(huì)發(fā)生這種情況。 是的,現(xiàn)在有點(diǎn)毛了。 這可能會(huì)導(dǎo)致問題,因?yàn)槲覀儾恢浪鼈兪欠駥耐晃恢眉虞d類。 怎么會(huì)這樣 讓我們看下面的代碼,它們?nèi)匀辉赥est類中:
Factory.instance().sayHello();該代碼看起來非常干凈和安全,尚不清楚如何從此行出現(xiàn)錯(cuò)誤。 我們正在調(diào)用靜態(tài)工廠方法來獲取Test類的實(shí)例,并在其上調(diào)用方法。 讓我們看一下該支持圖像,以顯示引發(fā)異常的原因。
在這里,我們可以看到一個(gè)Web類加載器(加載了Test類)將優(yōu)先使用本地類,因此,當(dāng)它引用一個(gè)類時(shí),將盡可能由Web類加載器加載。 到目前為止還算簡(jiǎn)單。 Test類使用Factory類來獲取Util類的實(shí)例,這在Java中是很典型的做法,但是Factory類在WAR中并不存在,因?yàn)樗且粋€(gè)外部庫。 這是沒有問題的,因?yàn)閃eb類加載器可以委托給共享類加載器,后者可以看到Factory類。 請(qǐng)注意,共享類加載器現(xiàn)在正在加載它自己的Util類版本,因?yàn)楫?dāng)Factory實(shí)例化該類時(shí),它使用了共享類加載器(如前面的第一個(gè)示例所示)。 Factory類將Util對(duì)象(由共享類加載器創(chuàng)建)返回給WAR,WAR然后嘗試使用該類,并將該類有效地強(qiáng)制轉(zhuǎn)換為同一類的潛在不同版本(Web類加載器可見的Util類) )。 繁榮!
我們可以在兩個(gè)地方(Factory.instance()方法和Test類)中運(yùn)行與以前相同的代碼,以查看每個(gè)Util類從何處加載。
Test.class.getClassLoader().getResource(Util.class.getName().replace('.', '/') + ".class")); 希望這可以使您對(duì)類加載的世界有所了解,而不是不了解類加載器,現(xiàn)在可以帶著恐懼和不確定性來欣賞它! 感謝您的閱讀并將其制作到最后。 我們都希望您從ZeroTurnaround祝您圣誕快樂,新年快樂! 編碼愉快!
參考: 在JVM的底層– Java出現(xiàn)日歷博客中來自JCG合作伙伴 Simon Maple的類加載器 。
翻譯自: https://www.javacodegeeks.com/2012/12/under-the-jvm-hood-classloaders.html
總結(jié)
以上是生活随笔為你收集整理的在JVM之下–类加载器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于偏爱的句子 表达被偏爱的句子
- 下一篇: 小心缓存管理器