據(jù)說99.99%的人都會(huì)答錯(cuò)的類加載問題
概述
首先還是把問題拋給大家,這個(gè)問題也是我廠同學(xué)在做一個(gè)性能分析產(chǎn)品的時(shí)候碰到的一個(gè)問題。
同一個(gè)類加載器對(duì)象是否可以加載同一個(gè)類文件多次并且得到多個(gè)Class對(duì)象而都可以被java層使用嗎?
請(qǐng)仔細(xì)注意上面的描述里幾個(gè)關(guān)鍵的詞:
- 同一個(gè)類加載器:意味著不是每次都new一個(gè)類加載器對(duì)象,我知道有些對(duì)類加載器有點(diǎn)理解的同學(xué)肯定會(huì)想到這點(diǎn)。我們這里強(qiáng)調(diào)的是同一個(gè)類加載器對(duì)象去加載。
- 同一個(gè)類文件:意味著類文件里的信息都一致,不存在修改的情況,至少名字不能改。因?yàn)橛行┩瑢W(xué)會(huì)鉆空子,比如說拿到類文件然后修改名字啥的,哈哈。
- 多個(gè)Class對(duì)象:意味著每次創(chuàng)建都是新的Class對(duì)象,并不是返回同一個(gè)Class對(duì)象。
- 都可以被java層使用:意味著Java層能感知到,或許對(duì)我公眾號(hào)關(guān)注挺久的同學(xué)看過我的一些文章,知道我這里說的是什么,不知道的可以翻翻我前面的文章,這里賣個(gè)關(guān)子,不直接告訴你哪篇文章,稍微提示一下和內(nèi)存GC有關(guān)。
雖然有些標(biāo)題黨的意思,不過我覺得標(biāo)題里的99.99%說得應(yīng)該不夸張,這個(gè)比例或許應(yīng)該更大,不過還是請(qǐng)認(rèn)真作答,不要隨便選,我知道肯定有人會(huì)隨便選的,哈哈。
正常的類加載
這里提正常的類加載,也是我們大家理解的類加載機(jī)制,不過我稍微說得深一點(diǎn),從JVM實(shí)現(xiàn)角度來說一下。在JVM里有一個(gè)數(shù)據(jù)結(jié)構(gòu)叫做SystemDictonary,這個(gè)結(jié)構(gòu)主要就是用來檢索我們常說的類信息,這些類信息對(duì)應(yīng)的結(jié)構(gòu)是klass,對(duì)SystemDictonary的理解,可以認(rèn)為就是一個(gè)Hashtable,key是類加載器對(duì)象+類的名字,value是指向klass的地址。這樣當(dāng)我們?nèi)我庖粋€(gè)類加載器去正常加載類的時(shí)候,就會(huì)到這個(gè)SystemDictonary中去查找,看是否有這么一個(gè)klass可以返回,如果有就返回它,否則就會(huì)去創(chuàng)建一個(gè)新的并放到結(jié)構(gòu)里,其中委托類加載過程我就不說了。
那這么一說看起來不可能出現(xiàn)同一個(gè)類加載器加載同一個(gè)類多次的情況。
正常情況下也確實(shí)是這樣的。
奇怪的現(xiàn)象
然而我們從java進(jìn)程的內(nèi)存結(jié)構(gòu)里卻看到過類似這樣的一些現(xiàn)象,以下是我們性能分析產(chǎn)品里的部分截圖:
在這個(gè)現(xiàn)象里,名字為java.lang.invoke.LambdaForm$BMH的類有多個(gè),并且其類加載器都是BootstrapClassLoader,也就是同一個(gè)類加載器居然加載了同一個(gè)類多次。這是我們的分析工具有問題嗎?顯然不是,因?yàn)槲覀儚膬?nèi)存里讀到的就是這樣的信息。
現(xiàn)象模擬
上面的這個(gè)現(xiàn)象看起來和lambda有一定關(guān)系,不過實(shí)際上并不僅僅lambda才有這種情況,我們可以來模擬一下
- public static void main(String args[]) throws Throwable {
- Field f = Unsafe.class.getDeclaredField("theUnsafe");
- f.setAccessible(true);
- Unsafe unsafe = (Unsafe) f.get(null);
- String filePath = "/Users/nijiaben/AA.class";
- byte[] buffer =getFileContent(filePath);
- Class<?> c1 = unsafe.defineAnonymousClass(UnsafeTest.class, buffer, null);
- Class<?> c2 = unsafe.defineAnonymousClass(UnsafeTest.class, buffer, null);
- System.out.println(c1 == c2);
- }
上述代碼其實(shí)就是通過Unsafe這個(gè)對(duì)象的defineAnonymousClass方法來加載同一個(gè)類文件兩遍得到兩個(gè)Class對(duì)象,最終我們輸出為false。這也就是說c1和c2其實(shí)是兩個(gè)不同的對(duì)象。
因?yàn)槲覀兊念愇募际且粯拥?,也就是字?jié)碼里的類名也是完全一樣的,因此在jvm里的類對(duì)象的名字其實(shí)也都是一樣的。不過這里我要提一點(diǎn)的是,如果將c1和c2的名字打印出來,會(huì)發(fā)現(xiàn)有些區(qū)別,分別會(huì)在類名后面加上一個(gè)/hashCode值,這個(gè)hash值是對(duì)應(yīng)的Class對(duì)象的hashCode值。這個(gè)其實(shí)是JVM里的一個(gè)特殊處理。
另外你無法通過java層面的其他api,比如Class.forName來獲取到這種class,所以你要保存好這個(gè)得到的Class對(duì)象才能后面繼續(xù)使用它。
defineAnonymousClass的解說
defineAnonymousClass這個(gè)方法比較特別,從名字上也看得出,是創(chuàng)建了一個(gè)匿名的類,不過這種匿名的概念和我們理解的匿名是不太一樣的。這種類的創(chuàng)建通常會(huì)有一個(gè)宿主類,也就是***個(gè)參數(shù)指定的類,這樣一來,這個(gè)創(chuàng)建的類會(huì)使用這個(gè)宿主類的定義類加載器來加載這個(gè)類,最關(guān)鍵的一點(diǎn)是這個(gè)類被創(chuàng)建之后并不會(huì)丟到上述的SystemDictonary里,也就是說我們通過正常的類查找,比如Class.forName等api是無法去查到這個(gè)類是否被定義過的。因此過度使用這種api來創(chuàng)建這種類在一定程度上會(huì)帶來一定的內(nèi)存泄露。
那有人就要問了,看不到啥好處,為啥要提供這種api,這么做有什么意義,大家可以去了解下JSR292。jvm通過InvokeDynamic可以支持動(dòng)態(tài)類型語言,這樣一來其實(shí)我們可以提供一個(gè)類模板,在運(yùn)行的時(shí)候加載一個(gè)類的時(shí)候先動(dòng)態(tài)替換掉常量池中的某些內(nèi)容,這樣一來,同一個(gè)類文件,我們通過加載多次,并且傳入不同的一些cpPatches,也就是defineAnonymousClass的第三個(gè)參數(shù), 這樣就能做到運(yùn)行時(shí)產(chǎn)生不同的效果。
主要是因?yàn)樵瓉淼腏VM類加載機(jī)制是不允許這種情況發(fā)生的,因?yàn)槲覀儗?duì)同一個(gè)名字的類只能被同一個(gè)類加載器加載一次,因而為了能支持動(dòng)態(tài)語言的特性,提供類似的api來達(dá)到這種效果。
總結(jié)
總的來說,正常情況下,同一個(gè)類文件被同一個(gè)類加載器對(duì)象只能加載一次,不過我們可以通過Unsafe的defineAnonymousClass來實(shí)現(xiàn)同一個(gè)類文件被同一個(gè)類加載器對(duì)象加載多遍的效果,因?yàn)椴]有將其放到SystemDictonary里,因此我們可以無窮次加載同一個(gè)類。這個(gè)對(duì)于絕大部分人來說是不太了解的,因此大家在面試的時(shí)候,你能講清楚我這文章里的情況,相信是一個(gè)加分項(xiàng),不過也可能被誤傷,因?yàn)槟愕拿嬖嚬僖部赡懿磺宄@種情況。
【本文是51CTO專欄作者李嘉鵬的原創(chuàng)文章,轉(zhuǎn)載請(qǐng)通過微信公眾號(hào)(你假笨,id:lovestblog)聯(lián)系作者本人獲取授權(quán)】