從頭開始學習->JVM(五):類加載器(下)【源碼分析】

前言

上一篇文章桅狠,《從頭開始學習JVM(四):類加載器(中)》截型,我們知道了類加載器的基本原理趴荸,但是知道了這些原理之后,我們對類加載器的底層的邏輯宦焦,算不上有多清楚明白发钝。

我們僅僅是意識到,類加載器的種類有多少赶诊,類加載器的加載機制雙親委派模型以及如何打破雙親委派模型等等等相關的一些原理笼平。

但是在這些原理的背后,是什么在支撐著呢舔痪?

想一想吧寓调,所謂的JVM,本身就是一直概念上的產(chǎn)物锄码,并不是物理上的東西夺英,那么作為JVM一部分的類加載器,以及我們的類加載流程滋捶,也只是我們在概念上的劃分痛悯。而支持著這些概念上劃分的,就是我們的代碼重窟。這些代碼里面载萌,有C++代碼,有java代碼,這些代碼扭仁,組合成了一個個閉合的邏輯垮衷,在現(xiàn)實層面,實現(xiàn)了我們所要實現(xiàn)的原理和概念理論乖坠。

今天這篇文章搀突,我們嘗試從代碼層面,來解讀我們的類加載器以及一些相關的類加載機制熊泵。

正文

1.Launcher類

由于BootStapClassLoader加載器是由C++語言寫的仰迁,是嵌入到了JVM內核中,也就是說顽分,從java代碼的層面我們是看不到的徐许,因此我們不去細究這個啟動類加載器。

JVM生成的第一個類是Launcher類怯邪,先看代碼绊寻,如下:

    public static void main(String[] args) {
        ClassLoader classLoader = Launcher.class.getClassLoader();
        System.out.println("加載器:" + classLoader);
    }

代碼運行結果為:

E:\jdk\bin\java.exe

加載器:null

Process finished with exit code 0

請注意,最后的根加載器打印出的是null悬秉,這不代表著就是真的null澄步,只是因為啟動類加載器是由C++寫的,所以在java中顯示就是為null和泌。

通過代碼運行的結果為null村缸,我們可以得知,這個類是由BootstrapClassLoader加載器加載的武氓。

也就是說梯皿,JVM在啟動的時候,就生成了BootstrapClassLoader加載器县恕,而BootrapClassLoader加載器會JVM啟動的第一時間就會去創(chuàng)建Launcher類的實例东羹。

那么,為什么要創(chuàng)建Laucher類的實例呢忠烛?我們先來看一下Launcher類的內部代碼属提,如下:

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
           //創(chuàng)建ExtClassLoader加載器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //通過創(chuàng)建的ExtClassLoader加載器來獲取AppClassLoader設置為默認加載器加載器。
            //并且將AppClassLoader設置為默認加載器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                    ;
                } catch (InstantiationException var6) {
                    ;
                } catch (ClassNotFoundException var7) {
                    ;
                } catch (ClassCastException var8) {
                    ;
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }
}

通過這段代碼美尸,我們發(fā)現(xiàn)冤议,Launcher類中有一個構造方法Launcher(),這個方法在Launcher創(chuàng)建的時候师坎,就會創(chuàng)建ExtClassLoader加載器恕酸,通過創(chuàng)建的ExtClassLoader加載器來創(chuàng)建AppClassLoader加載器,并且將AppClassLoader設置為默認的系統(tǒng)加載器(也就是當前線程的上下文類加載器)胯陋。

也因此蕊温,當我們想要獲取當前應用程序的AppClassLoader加載器的時候袱箱,可以通過以下代碼獲取:

    public static void main(String[] args) {
        ClassLoader classLoader = Launcher.getLauncher().getClassLoader();
        System.out.println("加載器:" + classLoader);
    }

運行結果如下:

E:\jdk\bin\java.exe

加載器:sun.misc.Launcher$AppClassLoader@18b4aac2

Process finished with exit code 0

然后寿弱,既然創(chuàng)建了ExtClassLoader加載器和AppClassLoader加載器犯眠,那么我們可以去看看這兩個加載器的源碼。請注意的是症革,這兩個加載器都是Launcher類的靜態(tài)內部類。

2.ExtclassLoader和AppClassLoader

ExtclassLoader:

static class ExtClassLoader extends URLClassLoader類鸯旁, {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            final File[] var0 = getExtDirs();

            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        int var1 = var0.length;

                        for(int var2 = 0; var2 < var1; ++var2) {
                            MetaIndex.registerDirectory(var0[var2]);
                        }

                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) {
                throw (IOException)var2.getException();
            }
        }
    }

ExtclassLoader加載器繼承了URLClassLoader類噪矛,URLClassLoader這個類是繼承了頂級類ClassLoader的,是ClassLoader類的一個擴展铺罢。

ExtclassLoader沒有重寫ClassLoader類的loadClass()方法艇挨,而是直接調用了頂級類ClassLoader的loadClass()方法。

AppClassLoader:

static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

        public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }

            if (this.ucp.knownToNotExist(var1)) {
                Class var5 = this.findLoadedClass(var1);
                if (var5 != null) {
                    if (var2) {
                        this.resolveClass(var5);
                    }

                    return var5;
                } else {
                    throw new ClassNotFoundException(var1);
                }
            } else {
                return super.loadClass(var1, var2);
            }
        }
    }

AppclassLoader加載器也繼承了URLClassLoader類韭赘。

AppclassLoader重寫了ClassLoader類的loadClass()方法缩滨。

3.繼承了ClassLoader的URLClassLoader類

java.net.URLClassLoader類是繼承了ClassLoader的一個類,是ClassLoader類的一個擴展泉瞻。

ClassLoader只能加載classpath下面的類脉漏,而URLClassLoader則可以加載任意路徑下的類。

一般動態(tài)加載類的時候袖牙,都是直接用Class.forName()這個方法侧巨,但這個方法只能創(chuàng)建程序中已經(jīng)引用的類,并且只能用包名的方法進行索引鞭达,比如Java.lang.String司忱,不能對一個.class文件或者一個不在程序引用里的.jar包中的類進行創(chuàng)建。

URLClassLoader提供了這個功能畴蹭,它讓我們可以通過以下幾種方式進行加載:

  1. 文件: (從文件系統(tǒng)目錄加載)
  2. jar包: (從Jar包進行加載)
  3. Http: (從遠程的Http服務進行加載)

4.抽象的類加載器ClassLoader(頂級類)

java.lang.ClassLoader是Java層面對類加載器的抽象坦仍,這個類規(guī)范了類加載的基本流程,是類加載中的頂級類叨襟,其中比較重要的屬性及方法如下:

  1. parent():父類加載器的引用繁扎,一個類加載器通常都會保存一個父類加載器的引用,用于實現(xiàn)雙親委派機制芹啥。
  2. loadClass()方法锻离,該方法為類加載器的核心方法,其中實現(xiàn)了雙親委派的邏輯墓怀。
    代碼如下:
public abstract class ClassLoader {
    //通過這個類汽纠,我們可以找到這個加載器的父類加載器
    private final ClassLoader parent;

    //name是要加載的類的名稱,resolve是判斷這個類是否要解析
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先傀履,檢查類是否已經(jīng)加載
            Class<?> c = findLoadedClass(name);
            
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
            //如果父類加載器不為null(也就是不是啟動類加載器)虱朵,那就調用父類加載器的loadClass()方法來加載這個類
                        c = parent.loadClass(name, false);
                    } else {
                    //如果父類加載器為null莉炉,那么就獲取到啟動類加載器BootstrapClassLoader來加載這個類
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果沒有找到類,則拋出ClassNotFoundException異常
                    // from the non-null parent class loader
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    //如果仍然沒有找到碴犬,那么就調用這個加載器本身的findClass()去找到這個類
                    c = findClass(name);

                    // 這是定義類加載器絮宁,記錄數(shù)據(jù)
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //如果傳進來的參數(shù)resolve為true,那么我們就去解析這個類
                resolveClass(c);
            }
            //返回這個類對應的Class對象
            return c;
        }
    }
    }

要說明一下服协,在這個loadClass()方法中绍昂,我們調用的方法大部分都是native方法,說白了都是使用了C++寫的偿荷,在java里面是看不到具體實現(xiàn)的窘游。

從以上代碼我們可以看出來,是實現(xiàn)了類加載機制之雙親委派模型的跳纳。

5.加載器之間的層級關系

類加載器有著一定的層級關系忍饰,比如說userClassLoader的父類加載器是AppClassLoader,而AppClassLoader的父類加載器是ExtClassLoader寺庄,而ExtClassLoader的父類加載器是BootStrapClassLoader艾蓝,而BootStrapClassLoader就是最高層級的類加載器了。

我先寫出如下一段代碼:

public static void main(String[] args) throws IOException {
        //獲取應用程序類加載器AppClassLoader
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("應用程序類加載器:" + appClassLoader);
        Enumeration<URL> enums = appClassLoader.getResources("");
        while (enums.hasMoreElements()) {
            System.out.println("應用程序類加載器加載路徑:"+enums.nextElement());
            break;
        }
        
        //獲取應用程序類加載器的父類加載器(也就是擴展類加載器ExtClassLoader)
        ClassLoader extClassLoader = appClassLoader.getParent();
        System.out.println("擴展類加載器:" + extClassLoader);
        System.out.println("擴展類加載器加載路徑:" + System.getProperty("java.ext.dirs"));
        
        //獲取擴展類加載器的父類加載器(也就是啟動類加載器BootStrapClassLoader)
        ClassLoader bootClassLoader = extClassLoader.getParent();
        System.out.println("根類加載器:" + bootClassLoader);
    }

這段代碼斗塘,運行的結果如下:

E:\jdk\bin\java.exe 

應用程序類加載器 :sun.misc.Launcher$AppClassLoader@18b4aac2

擴展類加載器 :sun.misc.Launcher$ExtClassLoader@156643d4

根類加載器 :null

Process finished with exit code 0

所以我們可以看到赢织,程序最后運行的結果,是和我們代碼里面所需要的結果逛拱,是一致的敌厘。也就是說,確實如我之前所說朽合,總結如下:

  1. userClassLoader的父類加載器是AppClassLoader俱两。
  2. AppClassLoader的父類加載器是ExtClassLoader。
  3. ExtClassLoader的父類加載器是BootStrapClassLoader曹步。
  4. BootStrapClassLoader是最高層級的類加載器宪彩。
  5. AppClassLoader是程序默認的類加載器。

6.類加載器的加載路徑

在上面那段代碼的基礎上讲婚,我們稍微修改一下代碼:

    public static void main(String[] args){
        //獲取到啟動類加載器BootStrapClassLoader
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for(URL url : urls) {
            System.out.println("BootstrapClassLoader的加載路徑: "+url);
            break;
        }
        
        //獲取到擴展類加載器ExtClassLoader
        URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent();
        urls = extClassLoader.getURLs();
        for(URL url : urls){
            System.out.println("ExtClassLoader的加載路徑: "+url);
            break;
        }
        
        //取得應用(系統(tǒng))類加載器AppClassLoader
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        Enumeration<URL> enums = appClassLoader.getResources("");
        while (enums.hasMoreElements()) {
            System.out.println("AppClassLoader的加載路徑:"+enums.nextElement());
            break;
        }
    }

得出的代碼的運行結果如下:

E:\jdk\bin\java.exe 

BootstrapClassLoader負責加載存放在 <JAVA_HOME>\lib目錄
2. 的加載路徑: file:/E:/jdk/jre/lib/resources.jar

ExtClassLoader的加載路徑: file:/E:/jdk/jre/lib/ext/access-bridge-64.jar

AppClassLoader的加載路徑: file:/L:/order-service/order-service-web/target/classes/

Process finished with exit code 0

因此尿孔,從最后的結果來看,我們可以獲取到這些類加載器的加載路徑筹麸,總結如下:

  1. BootstrapClassLoader負責加載存放在 <JAVA_HOME>\lib目錄
  2. ExtClassLoader主要加載JAVA中的一些拓展類活合,java.ext.dirs目錄中加載類庫,或者從JDK安裝目錄:jre/lib/ext目錄下加載類庫,是啟動類加載器的子類物赶。
  3. AppClassLoader負責加載環(huán)境變量classpath或者系統(tǒng)屬性java.class.path指定路徑下的類庫白指。

7.手寫一個UserClassLoader加載器

我們知道,實現(xiàn)屬于自己的類加載器UserClassLoader有兩種方式:

  1. 繼承java.lang.ClassLoader類酵紫,重寫findClass()方法
  2. 如果沒有太復雜的需求告嘲,可以直接繼承URLClassLoader類错维,重寫loadClass方法,具體可參考AppClassLoader和ExtClassLoader(我在上面已經(jīng)截圖說明了橄唬,請注意赋焕,這種方案會打破雙親委派模型)。

第二種方案仰楚,直接模擬AppClassLoader或者ExtClassLoader的源碼就可以了隆判,我們今天缸血,就來通過第一種方案,來手寫一個我們的UserClassLoader加載器捎泻。

7.1 創(chuàng)建一個加載器MyUserClassLoader

package com.blog.permission.classLoad;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;

public class MyUserClassLoader extends ClassLoader{
    private String rootPath;

    public MyUserClassLoader() {
        super();
    }

    public MyUserClassLoader(String rootPath) {
        this.rootPath = rootPath;
    }

    /**
     * 用于尋找類文件
     * @param className 類文件的全限定名
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String className)  {
        Class<?> clz = findLoadedClass(className);
        if (clz != null)
            return clz;
        byte[] classData = loadClassData(className);
        clz = defineClass(className, classData, 0, classData.length);
        return clz;
    }

    /**
     * 這是將文件轉換為二進制字節(jié)碼
     * @param className
     * @return
     */
    private byte[] loadClassData(String className) {
        String pathName = rootPath + className.replace(".", "/") + ".class";
        System.out.println(pathName);
        byte[] bytes = null;
        try (FileInputStream fis = new FileInputStream(pathName);
             ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
            byte[] flush = new byte[1024 * 1024];
            int len = -1;
            while (-1 != (len = fis.read(flush))) {
                bos.write(flush);
            }
            bytes = bos.toByteArray();
        } catch (Exception e) {
            System.out.println("異常");
        }
        return bytes;
    }
}

7.2 創(chuàng)建一個要加載的java類:Shuaige.java

package com.blog.permission.classLoad;


public class Shuaige {
    public static void main(String[] args) {
        System.out.println("I'm so cool,you underStand?");
    }
}

7.3 將shuaige.java文件編譯成Shuaige.class文件

image

7.4 創(chuàng)建一個運行的類

package com.blog.permission.classLoad;

public class TestClassLoad {

    public static void main(String[] args) throws ClassNotFoundException {
        MyUserClassLoader fileSystemClassLoader = new MyUserClassLoader("/com/blog/permission/classLoad");

        Class<?> c = fileSystemClassLoader.loadClass("com.blog.permission.classLoad.Shuaige");

        System.out.println(c);
    }
}

最后形成的層級目錄如下所示:


image

7.5 運行結果如下

E:\jdk\bin\java.exe 

class com.blog.permission.classLoad.Shuaige

Process finished with exit code 0

總結

通過對加載器的代碼的分析和解讀笆豁,很明顯闯狱,我們對類加載的機制有了更加深刻的了解。

當然抛计,某種程度上來講,本篇文章的源碼解析吹截,也只是解析了一部分而已,還有很多地方是沒有涉及到的波俄。但是透過這些源碼晨逝,我們對類加載器,有了質感上的提升懦铺,類加載器以及類加載過程捉貌,在我們心里,不再是虛擬的不落到實處的概念冬念,而是真真切切存在著的由代碼實現(xiàn)了的東西了趁窃。

到此,類加載器的整個解讀就已經(jīng)結束了急前。

參考博客

https://blog.csdn.net/how_interesting/article/details/80091472

https://blog.csdn.net/chuodan5158/article/details/100765519

https://www.cnblogs.com/chinaifae/p/10401523.html

https://blog.csdn.net/weixin_39161031/article/details/83000750

https://www.cnblogs.com/rogge7/p/7766522.html

參考書籍

周志明《深入理解java虛擬機》

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末醒陆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子叔汁,更是在濱河造成了極大的恐慌统求,老刑警劉巖检碗,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異码邻,居然都是意外死亡折剃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門像屋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怕犁,“玉大人,你說我怎么就攤上這事己莺∽喔Γ” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵凌受,是天一觀的道長阵子。 經(jīng)常有香客問我,道長胜蛉,這世上最難降的妖魔是什么挠进? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮誊册,結果婚禮上领突,老公的妹妹穿的比我還像新娘。我一直安慰自己案怯,他們只是感情好君旦,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嘲碱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捞魁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天谱俭,我揣著相機與錄音昆著,去河邊找鬼凑懂。 笑死梧宫,一個胖子當著我的面吹牛摆碉,可吹牛的內容都是我干的巷帝。 我是一名探鬼主播扫夜,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼笤闯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了超陆?” 一聲冷哼從身側響起浦马,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捐韩,失蹤者是張志新(化名)和其女友劉穎荤胁,沒想到半個月后屎债,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡圆丹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年辫封,在試婚紗的時候發(fā)現(xiàn)自己被綠了倦微。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片正压。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡焦履,死狀恐怖雏逾,靈堂內的尸體忽然破棺而出栖博,到底是詐尸還是另有隱情牧抵,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布妹孙,位于F島的核電站获枝,受9級特大地震影響,放射性物質發(fā)生泄漏嚣崭。R本人自食惡果不足惜懦傍,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一粗俱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧签财,春花似錦偏塞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沧卢。三九已至,卻和暖如春披诗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剥槐。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工粒竖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留几于,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓朽砰,卻偏偏與公主長得像喉刘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子造锅,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內容