Java虛擬機(jī) —— 類的加載機(jī)制

我們知道class文件中存儲(chǔ)了類的描述信息和各種細(xì)節(jié)的數(shù)據(jù),在運(yùn)行Java程序時(shí),虛擬機(jī)需要先將類的這些數(shù)據(jù)加載到內(nèi)存中,并經(jīng)過校驗(yàn)、轉(zhuǎn)換、解析和初始化過后,最終形成可以直接使用的Java類型。

類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為連接。


類的生命周期

類的加載機(jī)制實(shí)際上就是類的生命周期中加載、驗(yàn)證、準(zhǔn)備、解析、初始化5個(gè)過程。

加載

加載是類的加載過程的第一個(gè)階段,在加載階段,虛擬機(jī)需要完成以下3件事情:

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

通過全限定名來獲取二進(jìn)制流可以有很多種方式,比如從JAR、EAR、WAR文件包中讀取,從網(wǎng)絡(luò)獲取,也可以由其他文件來生成(jsp文件生成對應(yīng)的Servlet類),甚至還可以通過運(yùn)行時(shí)動(dòng)態(tài)生成(Java動(dòng)態(tài)代理)。

相比類加載過程的其他階段,加載階段是可控性最強(qiáng)的。因?yàn)殚_發(fā)者既可以利用系統(tǒng)提供的啟動(dòng)類加載器來完成,也可以通過自定義類加載去完成(重寫loadClass方法,控制字節(jié)流的獲取方式)。

關(guān)于類加載器的詳細(xì)介紹將放在文章最后。

加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對象,這樣就可以通過這個(gè)對象來訪問方法區(qū)中的這些數(shù)據(jù)。

驗(yàn)證

驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段大致上會(huì)完成下面4個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號引用驗(yàn)證。

  • 文件格式驗(yàn)證: 驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類型信息的要求。通過了這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),后面的
    3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流。
  • 元數(shù)據(jù)驗(yàn)證: 對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求。這個(gè)主要目的是對類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn),保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息。
  • 字節(jié)碼驗(yàn)證: 對類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件。
  • 符號驗(yàn)證: 對類自身以外(常量池中的各種符號引用)的信息進(jìn)行匹配性校驗(yàn),這個(gè)階段發(fā)生在將符號引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段中發(fā)生),目的是確保解析動(dòng)作能正常執(zhí)行。

準(zhǔn)備

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

這里有兩點(diǎn)需要注意:

  1. 成員變量不是在這里分配內(nèi)存的,成員變量是在類實(shí)例化對象的時(shí)候在堆中分配的。
  2. 這里設(shè)置初始值是指類型的零值(比如0,null,false等),而不是代碼中被顯示的賦予的值。

比如:

public class Test {
    public int number = 111;
    public static int sNumber = 111; 
}

成員變量number在這個(gè)階段就不會(huì)進(jìn)行內(nèi)存分配和初始化。而類變量sNunber會(huì)在方法區(qū)中分配內(nèi)存,并設(shè)置為int類型的零值0而不是111,賦值為111是在初始化階段才會(huì)執(zhí)行。

Java基本數(shù)據(jù)類型和引用數(shù)據(jù)類型零值

但是呢,如果類變量如果是被final修飾,為靜態(tài)常量,那么在準(zhǔn)備階段也會(huì)在方法區(qū)中分配內(nèi)存,并且將其值設(shè)置為顯示賦予的值。

比如:

public class Test {
    public static final int NUMBER = 111; 
}

此時(shí),就會(huì)在準(zhǔn)備階段將NUMBER的值設(shè)置為111。

解析

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

  • 符號引用: 符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。
  • 直接引用: 直接引用可以是直接指向目標(biāo)的指針、相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。

解析動(dòng)作主要就是在常量池中尋找類或接口、字段、類方法、接口方法、方法類型、方法句柄、調(diào)用點(diǎn)限定符等7類符號引用,把這些符號引用替換為直接引用。下面主要介紹下類或接口、字段、類方法、接口方法的解析:

  1. 類或接口解析: 假設(shè)當(dāng)前的類A通過符號X引用了類B,虛擬機(jī)會(huì)把代表類B的全限定名傳遞給A的類加載器去加載BB經(jīng)過加載、驗(yàn)證、準(zhǔn)備過程,在解析過程又可能會(huì)觸發(fā)B引用的其他的類的加載過程,相當(dāng)于一個(gè)類引用鏈的遞歸加載過程,整個(gè)過程只要不出現(xiàn)異常,B的就是一個(gè)加載成功的類或接口了,也就是可以獲取到代表Bjava.lang.Class對象。在驗(yàn)證了A具備對B的訪問權(quán)限后,就將符號引用X替換為B的直接引用。
  2. 字段解析: 解析未被解析過的字段,要先解析字段所屬的類或接口的符號引用。如果類本身就包含了簡單的名稱和字段描述與目標(biāo)字段相匹配,就直接返回這個(gè)字段引用;如果實(shí)現(xiàn)了接口,將會(huì)按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段;如果是繼承自其他類的話,將會(huì)按照繼承關(guān)系從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用。
  3. 類方法解析:類方法解析和字段解析的方式類似,也是依據(jù)繼承和實(shí)現(xiàn)關(guān)系從小到上搜索,只不過是先搜索類,后搜索接口。如果有簡單名稱和字段描述符都與目標(biāo)相匹配的字段,就返回字段引用。
  4. 接口的方法解析: 與類方法解析類似,從小到上搜索接口(接口沒有父類,只可能有父接口)。如果存在簡單名稱和字段描述符都與目標(biāo)相匹配的字段,就返回字段引用。

初始化

類的初始化類加載過程的最后一步,在前面的過中,除了在加載階段開發(fā)者可以自定義加載器之外,其余的動(dòng)作都是完全有虛擬機(jī)主導(dǎo)和控制完成。到了初始化階段,才真正開始執(zhí)行類中定義的Java代碼。

在準(zhǔn)備階段,類變量已經(jīng)設(shè)置了系統(tǒng)要求的零值,而在初始化階段,則根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另外一個(gè)角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動(dòng)收集類中所有的類變量(static變量)和靜態(tài)代碼塊(static{}塊)中的語句合并生成的。編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)代碼塊中只能訪問到定義在靜態(tài)代碼塊之前的變量,定義在它之后的變量,在前面的靜態(tài)代碼塊可以賦值,但是不能訪問。

public class Test {
    static {
        number = 111;               // 可以賦值
        System.out.println(number); // 不能讀取,編輯器或報(bào)錯(cuò)Illegal forward reference
    }
    static int number;
}

<clinit>()方法與類的構(gòu)造函數(shù)(或者說實(shí)例構(gòu)造器<init>()方法)不同,它不需要顯式地調(diào)用父類的<clinit>()方法,虛擬機(jī)會(huì)保證在子類的<clinit>()方法執(zhí)行之前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。所以,父類定義的靜態(tài)代碼塊要先與子類的賦值操作。

class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

class Sub extends Parent {
    public static int B = A;
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}

<clinit>()方法對于類或接口來說并不是必需的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法。

接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會(huì)生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法。

虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確地加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行<clinit>()方法完畢。如果在一個(gè)類的<clinit>()方法中有耗時(shí)很長的操作,就可能造成多個(gè)進(jìn)程阻塞。

類加載器

在之前的加載過程中,提到了類加載器通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流,這個(gè)過程可以讓開發(fā)中自定義類加載器來決定如何獲取需要的字節(jié)流。那么,什么是類加載器呢?

對于任意一個(gè)Java類,都必須通過類加載器加載到方法區(qū),并生成java.lang.Class對象才能使用類的各個(gè)功能,所以我們可以把類加載器理解為一個(gè)將class類文件轉(zhuǎn)換為java.lang.Class對象的工具。

對于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。也就是說,如果兩個(gè)類“相等”,那么這兩個(gè)類必須是被同一個(gè)虛擬機(jī)中的同一個(gè)類加載器加載,并且來自同一個(gè)class文件。

在Java當(dāng)中,已經(jīng)有3個(gè)預(yù)制的類加載器,分別是BootStrapClassLoaderExtClassLoader、AppClassLoader

  • BootStrapClassLoader: 啟動(dòng)類加載器,它是由C++來實(shí)現(xiàn)的,在Java程序中不能顯氏的獲取到。它負(fù)責(zé)加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下的類。
  • ExtClassLoader: 擴(kuò)展類加載器,它是由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫。開發(fā)者可以直接使用它。
  • AppClassLoader: 應(yīng)用程序類加載器,由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn),它負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類,開發(fā)者可以直接使用該類加載器。一般來說,開發(fā)者自定義的類就是由應(yīng)用程序類加載器加載的。

ExtClassLoader作為類加載器,但它也是一個(gè)Java類,是由BootStrapClassLoader來加載的,所以,ExtClassLoader的parent是BootStrapClassLoader。但是由于BootStrapClassLoaderc++實(shí)現(xiàn)的,我們通過ExtClassLoader.getParent獲取到的是null。同樣地,AppClassLoader是由ExtClassLoader加載,AppClassLoader的parent是ExtClassLoader

public class Test {
    public static void main(String[] args) {
        ClassLoader cl = Test.class.getClassLoader();
        while (cl != null) {
            System.out.println(cl);
            cl = cl.getParent();
        }
    }
}

打印結(jié)果:

sun.misc.Launcher$AppClassLoader@232204a1
sun.misc.Launcher$ExtClassLoader@74a14482

同時(shí)我們可以定義自己的類加載器CustomClassLoader,那么它的parent肯定就是AppClassLoader了。類加載器的這種層次關(guān)系稱為雙親委派模型。

類加載器
雙親委派模型

雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里類加載器之間的父子關(guān)系不是以繼承的關(guān)系來實(shí)現(xiàn),而是都使用遞歸的方式來調(diào)用父加載器的代碼。

雙親委派模型的工作過程是:如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求(它的搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。

ClassLoader的源碼:

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

先檢查是否已經(jīng)被加載過,若沒有加載則調(diào)用父類加載器的loadClass()方法,依次向上遞歸。若父類加載器為空則說明遞歸到啟動(dòng)類加載器了。如果從父類加載器到啟動(dòng)類加載器的上層次的所有加載器都加載失敗,則調(diào)用自己的findClass()方法進(jìn)行加載。

使用雙親委派模型能使Java類隨著加載器一起具備一種優(yōu)先級的層次關(guān)系,保證同一個(gè)類只加載一次,避免了重復(fù)加載,同時(shí)也能阻止有人惡意替換加載系統(tǒng)類。

自定義類加載器

一般地,在ClassLoader方法的loadClass方法中已經(jīng)給開發(fā)者實(shí)現(xiàn)了雙親委派模型,在自定義類加載器的時(shí)候,只需要復(fù)寫findClass方法即可。

public class CustomClassLoader extends ClassLoader {

    private String root;

    public CustomClassLoader(String root) {
        this.root = root;
    }

    @Override
    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 name) {
        String fileName = root + File.separatorChar
                + name.replace('.', File.separatorChar)
                + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

新建一個(gè)類com.xiao.U,編譯成class文件,放到桌面,來測試一下:

public class Test {
    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\PC\\Desktop");
        try {
            Class clazz = customClassLoader.loadClass("com.xiao.U");
            Object o = clazz.newInstance();
            System.out.println(o.getClass().getClassLoader());
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

打印結(jié)果:

CustomClassLoader@1540e19d

自定義類加載器在可以實(shí)現(xiàn)服務(wù)端的熱部署,在移動(dòng)端比如android也可以實(shí)現(xiàn)熱更新。


參考:

  1. 深入理解Java虛擬機(jī)(第二版)
  2. Java 類加載機(jī)制詳解
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評論 2 370

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