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

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網java

概述

首先仍是把問題拋給你們,這個問題也是我廠同窗在作一個性能分析產品的時候碰到的一個問題。面試

同一個類加載器對象是否能夠加載同一個類文件屢次而且獲得多個Class對象而均可以被java層使用嗎

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

  • 同一個類加載器:意味着不是每次都new一個類加載器對象,我知道有些對類加載器有點理解的同窗確定會想到這點。咱們這裏強調的是同一個類加載器對象去加載。
  • 同一個類文件:意味着類文件裏的信息都一致,不存在修改的狀況,至少名字不能改。由於有些同窗會鑽空子,好比說拿到類文件而後修更名字啥的,哈哈。
  • 多個Class對象:意味着每次建立都是新的Class對象,並非返回同一個Class對象。
  • 均可以被java層使用:意味着Java層能感知到,或許對我公衆號關注挺久的同窗看過個人一些文章,知道我這裏說的是什麼,不知道的能夠翻翻我前面的文章,這裏賣個關子,不直接告訴你哪篇文章,稍微提示一下和內存GC有關。

那接下來在看下面文章以前,我以爲你能夠先思考一個問題,網絡

同一類加載器對象是否可加載同一類文件屢次且獲得多個不一樣的Class對象(單選)
A.不知道 B.能夠 C.不能夠

雖然有些標題黨的意思,不過我以爲標題裏的99.99%說得應該不誇張,這個比例或許應該更大,不過仍是請認真做答,不要隨便選,我知道確定有人會隨便選的,哈哈。數據結構

正常的類加載

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

那這麼一說看起來不可能出現同一個類加載器加載同一個類屢次的狀況。工具

正常狀況下也確實是這樣的。性能

奇怪的現象

然而咱們從java進程的內存結構裏卻看到過相似這樣的一些現象,如下是咱們性能分析產品裏的部分截圖
image.png
在這個現象裏,名字爲java.lang.invoke.LambdaForm$BMH的類有多個,而且其類加載器都是BootstrapClassLoader,也就是同一個類加載器竟然加載了同一個類屢次。這是咱們的分析工具備問題嗎?顯然不是,由於咱們從內存裏讀到的就是這樣的信息。學習

現象模擬

上面的這個現象看起來和lambda有必定關係,不過實際上並不只僅lambda纔有這種狀況,咱們能夠來模擬一下spa

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裏,所以咱們能夠無窮次加載同一個類。這個對於絕大部分人來講是不太瞭解的,所以你們在面試的時候,你能講清楚我這文章裏的狀況,相信是一個加分項,不過也可能被誤傷,由於你的面試官也可能不清楚這種狀況,不過你能夠告訴他我這篇文章,哈哈,有收穫請幫忙點個好看,並分享出去,感謝。

一塊兒來學習吧

PerfMa KO 系列課之 JVM 參數【Memory篇】

java內存溢出問題分析過程