Java類加載器工作機制詳解

類加載的時機

類從被加載到虛擬機內(nèi)存中開始戒洼,到卸載出內(nèi)存為止掏愁,它的整個生命周期包括:加載(Loading)、驗證(Verification)场航、準備(Preparation)、解析(Resolution)廉羔、初始化(Initialization)溉痢、使用(Using)和卸載(Unloading)7個階段。其中 驗證憋他、準備孩饼、解析3個部分統(tǒng)稱為鏈接(Link),這7個階段的發(fā)生順序如下圖所示:

class_loading.png

類加載的過程

接下來我們詳細說一下Java虛擬機中類加載的全過程竹挡,也就是 加載镀娶、驗證、準備此迅、解析汽畴、初始化這5個階段所執(zhí)行的具體動作。

1、加載

"加載"是"類加載"(Class Loading)過程的一個階段。在加載階段宜鸯,虛擬機需要完成以下3件事:

  1. 通過一個的全限定名來獲取定義此類的二進制字節(jié)流岖沛。
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結構轉換為方法區(qū)的運行時數(shù)據(jù)結構。
  3. 在內(nèi)存中生成一個代表這個類的java.lang.Class對象廓握,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口搅窿。

2、驗證

驗證是 鏈接階段的第一步隙券,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求男应,并且不會危害虛擬機自身的安全。

驗證階段大致上會完成下面4個階段的校驗動作:文件格式驗證娱仔、元數(shù)據(jù)驗證沐飘、字節(jié)碼驗證、符號引用驗證。

3耐朴、準備

準備階段是正式為類變量分配內(nèi)存并設置變量初始值的階段借卧,這些變量所使用的內(nèi)存都將在方法區(qū)中的進行分配。

這個階段中有兩個容易產(chǎn)生混淆的地方:
首先筛峭,這時候進行內(nèi)存分配的僅包括類變量(被static修飾的變量)铐刘,而不包括示例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中影晓。
其次镰吵,這里所說的初始值 "通常情況"下是數(shù)據(jù)類型的默認值。

4挂签、解析

解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程疤祭。

5、初始化

類初始化是類加載過程中的最后一步竹握,前面的類加載過程中画株,除了在加載階段用戶應用程序可以通過 自定義類加載器參與之外,其余動作完全由虛擬機主導和控制啦辐。到了初始化階段谓传,才真正開始執(zhí)行類中定義的Java程序代碼(或者說字節(jié)碼)。

什么是類加載器

虛擬機設計團隊吧類加載階段中的 "通過一個類的全限定名來獲取描述此類的二進制字節(jié)流"這個動作放到虛擬機外部去實現(xiàn)芹关,以便讓應用程序自己覺得如何去獲取所需要的類续挟。實現(xiàn)這個動作的代碼模塊稱為 "類加載器"。

顧名思義侥衬,類加載器(ClassLoader )用來加載 Java 類到 Java 虛擬機中诗祸。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉換成 Java 字節(jié)代碼(.class 文件)轴总。類加載器負責讀取 Java 字節(jié)代碼直颅,并轉換成 java.lang.Class類的一個實例。每個這樣的實例用來表示一個 Java 類怀樟。

ClassLoader 類

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.

ClassLoader 中與加載類相關的方法

方法 說明
getParent() 返回該類加載器的父類加載器功偿。
loadClass(String name) 加載名稱為 name的類,返回的結果是 java.lang.Class類的實例往堡。
findClass(String name) 查找名稱為 name的類械荷,返回的結果是 java.lang.Class類的實例。
findLoadedClass(String name) 查找名稱為 name的已經(jīng)被加載過的類虑灰,返回的結果是 java.lang.Class類的實例吨瞎。
defineClass(String name, byte[] b, int off, int len) 把字節(jié)數(shù)組 b中的內(nèi)容轉換成 Java 類,返回的結果是 java.lang.Class類的實例穆咐。這個方法被聲明為 final的颤诀。
resolveClass(Class<?> c) 鏈接指定的 Java 類字旭。

loadClass(String name)方法源碼如下:

public abstract class ClassLoader {
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    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;
        }
    }
}

loadClass方法主要流程:

  1. 調(diào)用findLoadedClass(String) 方法檢查這個類是否被加載過,如果已加載則跳到第4步着绊,
  2. 如果父加載器存在則調(diào)用父加載器調(diào)用 loadClass(String) 方法谐算,否則,交給BootStrap ClassLoader 來加載此類归露,
  3. 如果執(zhí)行完上述2步驟 還沒有找到對應的類洲脂,則調(diào)用當前類加載器的findClass(String) 。
  4. 按照以上的步驟成功的找到對應的類剧包,并且該方法接收的 resolve 參數(shù)的值為 true,那么就調(diào)用resolveClass(Class) 方法來處理類恐锦。

類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實例緩存起來疆液。下次再請求加載該類的時候一铅,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載堕油。也就是說潘飘,對于一個類加載器實例來說,相同全名的類只加載一次掉缺,即 loadClass方法不會被重復調(diào)用卜录。

java.lang.ClassLoader類的方法 loadClass()封裝了類加載器的雙親委托模型的實現(xiàn)。因此眶明,為了保證類加載器都正確實現(xiàn) 類加載器的雙親委托模型艰毒,在開發(fā)自己的類加載器時,最好不要覆寫 loadClass()方法搜囱,而是覆寫 findClass()方法丑瞧。

ClassLoader 體系

Java 中的類加載器大致可以分成兩類,一類是系統(tǒng)提供的蜀肘,另外一類則是由 Java 應用開發(fā)人員編寫的绊汹。系統(tǒng)提供的類加載器主要有下面三個:

  • BootStrap ClassLoader:稱為啟動類加載器,是Java類加載層次中最頂層的類加載器扮宠,負責加載JDK中的核心類庫灸促,如:rt.jar、resources.jar涵卵、charsets.jar等,可通過如下程序獲得該類加載器從哪些地方加載了相關的jar或class文件:
public class BootStrapTest
{
    public static void main(String[] args)
    {
      URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
      for (int i = 0; i < urls.length; i++) {
          System.out.println(urls[i].toExternalForm());
       }
    }
}
  • Extension ClassLoader:稱為擴展類加載器荒叼,負責加載Java的擴展類庫轿偎,Java 虛擬機的實現(xiàn)會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類被廓。默認加載JAVA_HOME/jre/lib/ext/目下的所有jar坏晦。

  • App ClassLoader:稱為系統(tǒng)類加載器,負責加載應用程序classpath目錄下的所有jar和class文件。一般來說昆婿,Java 應用的類都是由它來完成加載的球碉。可以通過 ClassLoader.getSystemClassLoader()來獲取它仓蛆。

除了系統(tǒng)提供的類加載器以外睁冬,開發(fā)人員可以通過繼承java.lang.ClassLoader類的方式實現(xiàn)自己的類加載器,以滿足一些特殊的需求看疙。

         Bootstrap ClassLoader
                  |
         Extension ClassLoader
                  |
            App ClassLoader
              /           \
    自定義ClassLoader1   自定義ClassLoader2

除了引導類加載器之外豆拨,所有的類加載器都有一個父類加載器。 給出的 getParent()方法可以得到能庆。
對于系統(tǒng)提供的類加載器來說施禾,系統(tǒng)類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器搁胆;對于開發(fā)人員編寫的類加載器來說弥搞,其父類加載器是加載此類加載器 Java 類的類加載器。

類加載器的代理模式

類加載器在嘗試自己去查找某個類的字節(jié)代碼并定義它時渠旁,會先代理給其父類加載器攀例,由父類加載器先去嘗試加載這個類,依次類推一死。

當一個ClassLoader實例需要加載某個類時肛度,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器投慈,這個過程是由上至下依次檢查的承耿,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒加載到伪煤,則把任務轉交給Extension ClassLoader試圖加載加袋,如果也沒加載到,則轉交給App ClassLoader進行加載抱既,如果它也沒有加載得到的話职烧,則返回給委托的發(fā)起者,由它到指定的文件系統(tǒng)或網(wǎng)絡等URL中加載該類防泵。如果它們都沒有加載到這個類時蚀之,則拋出ClassNotFoundException異常。

這種機制又稱作做 類加載器的雙親委托模型捷泞。

Java 虛擬機是如何判定兩個 Class 是相同的

  • JVM在判定兩個class是否相同時足删,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的锁右。只有兩者同時滿足的情況下失受,JVM才認為這兩個class是相同的讶泰。*

即便是同樣的字節(jié)代碼,被不同的類加載器加載之后所得到的類拂到,也是不同的痪署。比如一個 Java 類 com.example.Sample,編譯之后生成了字節(jié)代碼文件 Sample.class兄旬。兩個不同的類加載器 ClassLoaderA和 ClassLoaderB
分別讀取了這個 Sample.class文件狼犯,并定義出兩個 java.lang.Class類的實例來表示這個類。這兩個實例是不相同的辖试。對于 Java 虛擬機來說辜王,它們是不同的類。試圖對這兩個類的對象進行相互賦值罐孝,會拋出運行時異常java.lang.ClassCastException呐馆。
下面通過示例來具體說明。
Java 類 com.example.Sample:

 package com.example; 

 public class Sample { 
    private Sample instance; 

    public void setSample(Object instance) { 
        this.instance = (Sample) instance; 
    } 
 }

ClassIdentityDemo

public class ClassIdentityDemo {

    public static void main(String[] args) {

        new ClassIdentityDemo().testClassIdentity();
    }

    public void testClassIdentity() {
        String classDataRootPath = "F:\\github\\daily-codelab\\classloader-sample\\target\\classes";
        FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
        FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
        String className = "com.bytebeats.classloader.sample.ch3.Sample";
        try {
            Class<?> class1 = fscl1.loadClass(className);
            Object obj1 = class1.newInstance();
            Class<?> class2 = fscl2.loadClass(className);
            Object obj2 = class2.newInstance();
            Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
            setSampleMethod.invoke(obj1, obj2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

FileSystemClassLoader

import java.io.*;

/**
 * ${DESCRIPTION}
 *
 * @author Ricky Fung
 * @create 2017-03-12 17:24
 */
public class FileSystemClassLoader extends ClassLoader {
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

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

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream in = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = in.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

}

為什么要使用代理模式

了解了這一點之后莲兢,就可以理解代理模式的設計動機了汹来。代理模式是為了保證 Java 核心庫的類型安全。所有 Java 應用都至少需要引用 java.lang.Object類改艇,也就是說在運行的時候收班,java.lang.Object這個類需要被加載到 Java 虛擬機中。如果這個加載過程由 Java 應用自己的類加載器來完成的話谒兄,很可能就存在多個版本的 java.lang.Object類摔桦,而且這些類之間是不兼容的。通過代理模式承疲,對于 Java 核心庫的類的加載工作由引導類加載器來統(tǒng)一完成邻耕,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的燕鸽。

不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間兄世。相同名稱的類可以并存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可啊研。不同類加載器加載的類之間是不兼容的御滩,這就相當于在 Java 虛擬機內(nèi)部創(chuàng)建了一個個相互隔離的 Java 類空間。這種技術在許多框架中都被用到党远,例如OSGI削解、Web 容器(Tomcat)。

類加載器與 Web 容器

對于運行在 Java EE?容器中的 Web 應用來說沟娱,類加載器的實現(xiàn)方式與一般的 Java 應用有所不同钠绍。不同的 Web 容器的實現(xiàn)方式也會有所不同。以 Apache Tomcat 來說花沉,每個 Web 應用都有一個對應的類加載器實例柳爽。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類碱屁,如果找不到再代理給父類加載器磷脯。這與一般類加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法娩脾,其目的是使得 Web 應用自己的類的優(yōu)先級高于 Web 容器提供的類赵誓。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫的類型安全柿赊。

參考資料

深入探討 Java 類加載器

源代碼

所有源碼均已上傳給Github

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俩功,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子碰声,更是在濱河造成了極大的恐慌诡蜓,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胰挑,死亡現(xiàn)場離奇詭異蔓罚,居然都是意外死亡,警方通過查閱死者的電腦和手機瞻颂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門豺谈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贡这,你說我怎么就攤上這事茬末。” “怎么了盖矫?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵丽惭,是天一觀的道長。 經(jīng)常有香客問我炼彪,道長吐根,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任辐马,我火速辦了婚禮拷橘,結果婚禮上,老公的妹妹穿的比我還像新娘喜爷。我一直安慰自己冗疮,他們只是感情好,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布檩帐。 她就那樣靜靜地躺著术幔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪湃密。 梳的紋絲不亂的頭發(fā)上诅挑,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天四敞,我揣著相機與錄音,去河邊找鬼拔妥。 笑死忿危,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的没龙。 我是一名探鬼主播铺厨,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼硬纤!你這毒婦竟也來了解滓?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤筝家,失蹤者是張志新(化名)和其女友劉穎洼裤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肛鹏,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡逸邦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了在扰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缕减。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖芒珠,靈堂內(nèi)的尸體忽然破棺而出桥狡,到底是詐尸還是另有隱情,我是刑警寧澤皱卓,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布裹芝,位于F島的核電站,受9級特大地震影響娜汁,放射性物質發(fā)生泄漏嫂易。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一掐禁、第九天 我趴在偏房一處隱蔽的房頂上張望怜械。 院中可真熱鬧,春花似錦傅事、人聲如沸缕允。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽障本。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驾霜,已是汗流浹背案训。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留寄悯,地道東北人萤衰。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像猜旬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倦卖,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

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