Categories
程式開發

據說99.99%的人都會答錯的類加載的問題


概述

首先還是把問題拋給大家,這個問題也是我廠同學在做一個性能分析產品的時候碰到的一個問題。

同一個類加載器對像是否可以加載同一個類文件多次並且得到多個Class對象而都可以被java層使用嗎

請仔細注意上面的描述裡幾個關鍵的詞

同一個類加載器:意味著不是每次都new一個類加載器對象,我知道有些對類加載器有點理解的同學肯定會想到這點。我們這裡強調的是同一個類加載器對象去加載。同一個類文件:意味著類文件裡的信息都一致,不存在修改的情況,至少名字不能改。因為有些同學會鑽空子,比如說拿到類文件然後修改名字啥的,哈哈。多個Class對象:意味著每次創建都是新的Class對象,並不是返回同一個Class對象。都可以被java層使用:意味著Java層能感知到,或許對我公眾號關注挺久的同學看過我的一些文章,知道我這裡說的是什麼,不知道的可以翻翻我前面的文章,這裡賣個關子,不直接告訴你哪篇文章,稍微提示一下和內存GC有關。

那接下來在看下面文章之前,我覺得你可以先思考一個問題,

同一類加載器對像是否可加載同一類文件多次且得到多個不同的Class對象(單選)

A.不知道B.可以C.不可以

雖然有些標題黨的意思,不過我覺得標題裡的99.99%說得應該不誇張,這個比例或許應該更大,不過還是請認真作答,不要隨便選,我知道肯定有人會隨便選的,哈哈。

正常的類加載

這裡提正常的類加載,也是我們大家理解的類加載機制,不過我稍微說得深一點,從JVM實現角度來說一下。在JVM裡有一個數據結構叫做SystemDictonary,這個結構主要就是用來檢索我們常說的類信息,這些類信息對應的結構是klass,對SystemDictonary的理解,可以認為就是一個Hashtable,key是類加載器對象+類的名字,value是指向klass的地址。這樣當我們任意一個類加載器去正常加載類的時候,就會到這個SystemDictonary中去查找,看是否有這麼一個klass可以返回,如果有就返回它,否則就會去創建一個新的並放到結構裡,其中委託類加載過程我就不說了。

那這麼一說看起來不可能出現同一個類加載器加載同一個類多次的情況。

正常情況下也確實是這樣的。

奇怪的現象

然而我們從java進程的內存結構裡卻看到過類似這樣的一些現象,以下是我們性能分析產品裡的部分截圖

據說99.99%的人都會答錯的類加載的問題 1

在這個現象裡,名字為java.lang.invoke.LambdaForm$BMH的類有多個,並且其類加載器都是BootstrapClassLoader,也就是同一個類加載器居然加載了同一個類多次。這是我們的分析工具有問題嗎?顯然不是,因為我們從內存裡讀到的就是這樣的信息。

現像模擬

上面的這個現像看起來和lambda有一定關係,不過實際上並不僅僅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);
}

上述代碼其實就是通過Unsafe這個對象的defineAnonymousClass方法來加載同一個類文件兩遍得到兩個Class對象,最終我們輸出為false。這也就是說c1和c2其實是兩個不同的對象。

因為我們的類文件都是一樣的,也就是字節碼裡的類名也是完全一樣的,因此在jvm裡的類對象的名字其實也都是一樣的。不過這裡我要提一點的是,如果將c1和c2的名字打印出來,會發現有些區別,分別會在類名後面加上一個/hashCode值,這個hash值是對應的Class對象的hashCode值。這個其實是JVM裡的一個特殊處理。

另外你無法通過java層面的其他api,比如Class.forName來獲取到這種class,所以你要保存好這個得到的Class對象才能後面繼續使用它。

defineAnonymousClass的解說

defineAnonymousClass這個方法比較特別,從名字上也看得出,是創建了一個匿名的類,不過這種匿名的概念和我們理解的匿名是不太一樣的。這種類的創建通常會有一個宿主類,也就是第一個參數指定的類,這樣一來,這個創建的類會使用這個宿主類的定義類加載器來加載這個類,最關鍵的一點是這個類被創建之後並不會丟到上述的SystemDictonary裡,也就是說我們通過正常的類查找,比如Class.forName等api是無法去查到這個類是否被定義過的。因此過度使用這種api來創建這種類在一定程度上會帶來一定的內存洩露。

那有人就要問了,看不到啥好處,為啥要提供這種api,這麼做有什麼意義,大家可以去了解下JSR292。 jvm通過InvokeDynamic可以支持動態類型語言,這樣一來其實我們可以提供一個類模板,在運行的時候加載一個類的時候先動態替換掉常量池中的某些內容,這樣一來,同一個類文件,我們通過加載多次,並且傳入不同的一些cpPatches,也就是defineAnonymousClass的第三個參數, 這樣就能做到運行時產生不同的效果。

主要是因為原來的JVM類加載機制是不允許這種情況發生的,因為我們對同一個名字的類只能被同一個類加載器加載一次,因而為了能支持動態語言的特性,提供類似的api來達到這種效果。

總結

總的來說,正常情況下,同一個類文件被同一個類加載器對像只能加載一次,不過我們可以通過Unsafe的defineAnonymousClass來實現同一個類文件被同一個類加載器對象加載多遍的效果,因為並沒有將其放到SystemDictonary裡,因此我們可以無窮次加載同一個類。這個對於絕大部分人來說是不太了解的,因此大家在面試的時候,你能講清楚我這文章裡的情況,相信是一個加分項,不過也可能被誤傷,因為你的面試官也可能不清楚這種情況,不過你可以告訴他我這篇文章,哈哈,有收穫請幫忙點個好看,並分享出去,感謝。

看完三件事❤️

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

點贊,轉發,有你們的『點贊和評論』,才是我創造的動力。關注公眾號『 java爛豬皮』,不定期分享原創知識。同時可以期待後續文章ing🚀

據說99.99%的人都會答錯的類加載的問題 2

本文作者:你假笨出處:https://club.perfma.com/article/282222