深入理解JVM(2)—類加載器與類加載機(jī)制

類加載器(ClassLoader)是負(fù)責(zé)讀取 Java 字節(jié)碼,并轉(zhuǎn)換成 java.lang.Class 類的一個(gè)實(shí)例的代碼模塊。
類加載器除了用于加載類外,還可用于確定類在Java虛擬機(jī)中的唯一性。在整個(gè)JVM里蝇率,縱然全限定名相同,若類加載器不同刽沾,則仍然不算作是同一個(gè)類本慕,無法通過 instanceOf 、equals 等方式的校驗(yàn)侧漓。

1. 類加載器

類加載器層級(jí)關(guān)系如下圖所示锅尘, Bootstrap、Extension布蔗、Application 三者并非是繼承關(guān)系藤违,而是子類加載器指派父類加載器為自己的 parent屬性。由于啟動(dòng)類加載器是內(nèi)嵌于 JVM 且無法被引用纵揍,因此 Extension Classloader 設(shè)置null為parent顿乒,即等同于指派啟動(dòng)類加載器為自己的父加載器。

類加載器

1.1 Bootstrap ClassLoader
由C++語言實(shí)現(xiàn)的泽谨,并不是繼承自java.lang.ClassLoader淆游,沒有父類加載器。
加載Java核心類庫(kù)隔盛,如:$JAVA_HOME/jre/lib/rt.jar、resources.jar拾稳、sun.boot.class.path路徑下的包吮炕,用于提供JVM運(yùn)行所需的包。
它加載擴(kuò)展類加載器和應(yīng)用程序類加載器访得,并成為他們的父類加載器龙亲。

1.2 Extension ClassLoader
Java語言編寫,繼承自java.lang.ClassLoader悍抑,父類加載器為啟動(dòng)類加載器鳄炉。
負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包。從系統(tǒng)屬性:java.ext.dirs目錄中加載類庫(kù)搜骡,或者從JDK安裝目拂盯。錄:jre/lib/ext目錄下加載類庫(kù)。我們可以將我們自己的包放在以上目錄下记靡,就會(huì)自動(dòng)加載進(jìn)來了谈竿。

1.3 Application ClassLoader
Java語言編寫团驱,繼承自java.lang.ClassLoader,父類加載器為啟動(dòng)類加載器
它負(fù)責(zé)加載環(huán)境變量classpath或者系統(tǒng)屬性java.class.path指定路徑下的類庫(kù)
是程序中默認(rèn)的類加載器空凸,我們Java程序中的類嚎花,都是由它加載完成的。
我們可以通過ClassLoader#getSystemClassLoader()獲取并操作這個(gè)加載器呀洲。

1.4 User ClassLoader
Java語言編寫紊选,根據(jù)自身需要實(shí)現(xiàn)ClassLoader自定義加載class,如tomcat道逗、jboss都會(huì)根據(jù)j2ee規(guī)范自行實(shí)現(xiàn)ClassLoader兵罢。通過靈活定義classloader的加載機(jī)制,我們可以完成很多事情憔辫,例如解決類沖突問題趣些,實(shí)現(xiàn)熱加載以及熱部署,甚至可以實(shí)現(xiàn)jar包的加密保護(hù)贰您。實(shí)現(xiàn)自定義ClassLoader的示例參考章節(jié)3自定義類加載器

  // App ClassLoader
  System.out.println(this.getClass().getClassLoader());
  // Ext ClassLoader
  System.out.println(this.getClass().getClassLoader().getParent());
  // Bootstrap ClassLoader
  System.out.println(this.getClass().getClassLoader().getParent().getParent());
  // Bootstrap ClassLoader
  System.out.println(new String().getClass().getClassLoader());

輸出結(jié)果如下坏平,Bootstrap ClassLoader屬于JVM的范疇,所以是null

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3ac42916
null
null

3. 加載類的方式

1. 隱式加載
通過new隱式加載锦亦,創(chuàng)建對(duì)象時(shí)舶替,如果類未加載也會(huì)嘗試加載,例如 Student s = new Student();會(huì)嘗試加載Student
2. 顯示加載:

  1. 通過Class.forName(類全限定名)加載
    public static Class<?> forName(String className) throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

其中forName0() 方法調(diào)用中的參數(shù) true 表示要初始化該類杠园。包括:

  • 執(zhí)行靜態(tài)代碼塊
  • 初始化靜態(tài)域

例如com.mysql.jdbc.Driver就通過靜態(tài)代碼塊想DriverManager中注冊(cè)自己顾瞪。當(dāng)然forName(String name, boolean initialize,ClassLoader loader)也支持指定初始化(initialize)和類加載器(loader)

  1. 通過ClassLoaderloadClass()方法加載類

4. 類加載機(jī)制

4.1 全盤負(fù)責(zé)
當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)類時(shí),該類所依賴的和引用的其他類也將由該類加載器負(fù)責(zé)加載抛蚁,除非顯示使用另外一個(gè)類加載器來加載陈醒。
4.2 雙親委派
雙親委派(Parent Delegation),是一個(gè)非常糟糕的翻譯瞧甩,但是因?yàn)槭褂幂^廣所以一直沿用至今钉跷, 雙親委派也叫作“父類委托”,是指子類加載器如果沒有加載過該目標(biāo)類肚逸,就先委托父類加載器加載該目標(biāo)類爷辙,只有在父類加載器找不到字節(jié)碼文件的情況下才從自己的類路徑中查找并加載目標(biāo)類。
雙親委派的機(jī)制如ClassLoader中l(wèi)oadClass方法所示:

  1. findLoadedClass朦促,內(nèi)部調(diào)用native方法膝晾,在虛擬機(jī)內(nèi)存中查找是否已經(jīng)加載過此類
  2. 如果沒有加載,則委派父類加載务冕,對(duì)于Extension ClassLoader血当,parent是null,所以通過findBootstrapClassOrNull委派Bootstrap ClassLoader加載。
  3. 如果父類沒有加載歹颓,則通過findClass(name)自己嘗試加載坯屿。
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

5. 自定義類加載器

“雙親委派”機(jī)制只是Java推薦的,并不是強(qiáng)制的機(jī)制巍扛。我們可以繼承java.lang.ClassLoader類领跛,實(shí)現(xiàn)自己的類加載器。如果想保持雙親委派模型撤奸,只需要重寫findClass(name)方法吠昭;如果想破壞雙親委派模型,則重寫loadClass(name)方法胧瓜。
如下MyClassLoader矢棚,通過重寫findClass,從MyClassLoader.setRoot 指定的目錄加載編譯后的.class 文件府喳。注意文件路徑不能是classpath路徑蒲肋, 防止要加載的類被Application ClassLoader加載。

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        // fileName處理邏輯需要根據(jù)實(shí)際情況修改钝满,保證能夠找到文件
        String[] name = className.split("\\.");
        String fileName = root + File.separatorChar + name[name.length - 1] + ".class";
        try {
            return Files.readAllBytes(Paths.get(fileName));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("指定目錄");
        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("類的全限定名");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
            System.out.println(object.getClass().getClassLoader().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6. SPI機(jī)制是否打破雙親委派

關(guān)于SPI(Service Provider Interface)是否打破雙親委派兜粘,眾說紛紜,這里先拋出結(jié)論:沒有打破雙親委派弯蚜。
雙親委派機(jī)制是指孔轴,子類加載器加載類之前,先去父類加載器中查找碎捺,一直查到最基礎(chǔ)的啟動(dòng)類加載器路鹰,如果都沒有加載,則嘗試自行加載收厨。根據(jù)雙親委派原理晋柱,父類加載器加載的類對(duì)子類可見,反之則不成立诵叁。
這里以JDBC(Java Database Connectivity趣斤,Java數(shù)據(jù)庫(kù)連接池)為例進(jìn)行說明。使用JDBC的示例代碼如下:

String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
Connection conn = DriverManager.getConnection(url, "root", "1234");

1. DriverManager加載
根據(jù)包名可知java.sql.DriverManager是由啟動(dòng)類加載器加載黎休,在加載時(shí),通過靜態(tài)代碼塊調(diào)用loadInitialDrivers()方法玉凯, loadInitialDrivers()通過SPI的方式加載java.sql.Driver的實(shí)現(xiàn)势腮,這里是com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver,加載過程如下漫仆,省略部分非核心代碼:

    private static void loadInitialDrivers() {
        String drivers;

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
    }

2. java.mysql.jdbc.Driver 加載
SPI的機(jī)制這里不展開講捎拯,核心原理是通過mysql-connector-java.jar中的META-INF/services/java.sql.Driver文件,找到java.sql.Driver的具體實(shí)現(xiàn)盲厌,然后加載實(shí)現(xiàn)署照。
loadInitialDrivers()中的load() 實(shí)現(xiàn)如下所示祸泪,注意通過Thread.currentThread().getContextClassLoader()獲得應(yīng)用類加載器,賦值給loader成員變量建芙。

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

SPI中没隘,最終通過Thread.currentThread().getContextClassLoader()獲得的應(yīng)用類加載器加載com.mysql.jdbc.Driver,代碼如下:

        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,  "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service, "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        }

cn這里就是com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver禁荸。調(diào)用c.newInstances時(shí)右蒲, 會(huì)執(zhí)行com.mysql.jdbc.Driver中的靜態(tài)代碼塊,即向DriverManager注冊(cè)自己赶熟。

以上穿插的源碼比較多瑰妄,又夾雜著SPI的源碼,所以比較混亂映砖,這里做一下總結(jié):

  1. java.sql.DriverManager 是由啟動(dòng)類加載器加載间坐,在加載時(shí),通過SPI加載java.sql.Driver的實(shí)現(xiàn)邑退,即com.mysql.jdbc.Driver竹宋。
  2. com.mysql.jdbc.Driver是由應(yīng)用類加載器負(fù)責(zé)加載的
  3. 父類加載器(Bootstrap ClassLoader)通過線程上下文類加載器(ContextClassLoader)去請(qǐng)求子類加載器(Application ClassLoader)完成類加載的行為,看似打破了雙親委派模型來逆向使用類加載器瓜饥,晚上所有的打破雙親委派也是指這一過程逝撬。但是子類加載器的加載也是走雙親委派流程,先委托給父類加載器乓土,加載不到再嘗試自行加載宪潮,因此完全沒有破壞雙親委派。


    image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末趣苏,一起剝皮案震驚了整個(gè)濱河市狡相,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌食磕,老刑警劉巖尽棕,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異彬伦,居然都是意外死亡滔悉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門单绑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來回官,“玉大人,你說我怎么就攤上這事搂橙∏柑幔” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)苔巨。 經(jīng)常有香客問我版扩,道長(zhǎng)侄泽,這世上最難降的妖魔是什么礁芦? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蔬顾,結(jié)果婚禮上宴偿,老公的妹妹穿的比我還像新娘。我一直安慰自己诀豁,他們只是感情好窄刘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舷胜,像睡著了一般娩践。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烹骨,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天翻伺,我揣著相機(jī)與錄音,去河邊找鬼沮焕。 笑死吨岭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的峦树。 我是一名探鬼主播辣辫,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼魁巩!你這毒婦竟也來了急灭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤谷遂,失蹤者是張志新(化名)和其女友劉穎葬馋,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肾扰,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡畴嘶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了集晚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掠廓。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖甩恼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站环凿,受9級(jí)特大地震影響遗增,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜磅轻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧踏枣,春花似錦、人聲如沸钙蒙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躬厌。三九已至马昨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扛施,已是汗流浹背鸿捧。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疙渣,地道東北人匙奴。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像妄荔,于是被迫代替她去往敵國(guó)和親泼菌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容