Java類(lèi)加載過(guò)程

概述

Java類(lèi)加載過(guò)程包括以下五個(gè)階段:

  • 加載
  • 驗(yàn)證
  • 準(zhǔn)備
  • 解析
  • 初始化
    驗(yàn)證、準(zhǔn)備和解析三個(gè)階段統(tǒng)稱(chēng)連接階段。
    加載、驗(yàn)證、準(zhǔn)備和初始化這幾個(gè)階段的開(kāi)始順序是確定的,解析階段不一定,可能會(huì)在初始化之后才開(kāi)始,也因此使得Java支持動(dòng)態(tài)綁定。
    詳細(xì)了解下各個(gè)階段具體的動(dòng)作。

加載

加載階段完成的是class文件的字節(jié)流載入虛擬機(jī),虛擬機(jī)在此階段需要完成以下三個(gè)任務(wù):

  1. 通過(guò)全限定類(lèi)名獲取類(lèi)的二進(jìn)制字節(jié)流(不管文件的來(lái)源,網(wǎng)絡(luò)也好,磁盤(pán)文件或壓縮包也罷,只要能讀入二進(jìn)制流就可以);
  2. 將讀取的字節(jié)流代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu);
  3. 在內(nèi)存中(并沒(méi)有明確規(guī)定是在堆中,HotSpot中Class對(duì)象雖然也是對(duì)象,但是卻是存儲(chǔ)在方法區(qū)中的)生成一個(gè)代表此類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種信息的入口。

在加載過(guò)程中,會(huì)用到類(lèi)加載器,類(lèi)加載器負(fù)責(zé)加載類(lèi)文件到虛擬機(jī)中,并返回Class對(duì)象。
需要注意的是,數(shù)組類(lèi)本身是不通過(guò)類(lèi)加載器加載的,它是由java虛擬機(jī)直接創(chuàng)建的。數(shù)組類(lèi)也是Class對(duì)象,是比較特殊的類(lèi):

package jdk.test.classLoader;

public class ArrayLoadTest {

    public static void main(String[] args) {
        Test[] test = new Test[10];
        int[] i = { 0 };
        System.out.println(test.getClass());
        System.out.println(test.getClass().getClassLoader());
        System.out.println(i.getClass());
        System.out.println(i.getClass().getClassLoader());
    }
}
// class [Ljdk.test.classLoader.Test;
// sun.misc.Launcher$AppClassLoader@73d16e93

// class [I
// null

從代碼執(zhí)行結(jié)果看,class [Ljdk.test.classLoader.Test;類(lèi)是由AppClassLoader加載的啊,int數(shù)組類(lèi)的加載器是null,那么這個(gè)null是不是BootStrapClassLoader呢?

一個(gè)數(shù)組類(lèi)的創(chuàng)建過(guò)程遵循以下原則:

  • 數(shù)組元素類(lèi)型是引用類(lèi)型,如上面的test,會(huì)使用元素類(lèi)的類(lèi)加載器去加載這個(gè)Test類(lèi),但是數(shù)組會(huì)在這個(gè)類(lèi)加載器的類(lèi)名稱(chēng)空間上被標(biāo)識(shí)。
  • 如果數(shù)組元素是基本類(lèi)型jvm會(huì)將此數(shù)組標(biāo)記為何引導(dǎo)類(lèi)加載器關(guān)聯(lián)。

可以使用java -verbose命令行選項(xiàng)查看類(lèi)加載的軌跡。

驗(yàn)證

驗(yàn)證階段主要對(duì)輸入的字節(jié)碼進(jìn)行檢查,確保其內(nèi)容符合規(guī)范。
實(shí)際上在加載期間就已經(jīng)開(kāi)始了驗(yàn)證過(guò)程,在讀入字節(jié)流之后需要經(jīng)過(guò)驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,且能被當(dāng)前虛擬機(jī)執(zhí)行,驗(yàn)證通過(guò)后才會(huì)存儲(chǔ)到方法區(qū)中,在之后的其他驗(yàn)證都是對(duì)方法區(qū)的數(shù)據(jù)進(jìn)行驗(yàn)證的,所以驗(yàn)證和加載時(shí)交叉的。

準(zhǔn)備

準(zhǔn)備階段的任務(wù)是給靜態(tài)變量分配內(nèi)存和設(shè)置初始值(零值),final static變量(實(shí)際上就是常量)會(huì)直接賦代碼中實(shí)際要賦的值,這些變量都將在方法區(qū)中分配內(nèi)存。

解析

解析階段是虛擬機(jī)將常量池中的符號(hào)引用替換成直接引用的過(guò)程。
通俗地說(shuō)就是把占位符換成真正的地址(內(nèi)存偏移等)
使用javap -verbose反編譯class文件可以看到常量池。

$ javac -verbose Test.java
[解析開(kāi)始時(shí)間 RegularFileObject[Test.java]]
[解析已完成, 用時(shí) 78 毫秒]
[源文件的搜索路徑: .,G:\Program Files\Java\jdk1.8.0_45\lib]
[類(lèi)文件的搜索路徑: G:\Program Files\Java\jdk1.8.0_45\jre\lib\resources.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\rt.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\sunrsasign.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\jsse.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\jce.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\charsets.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\jfr.jar,G:\Program Files\Java\jdk1.8.0_45\jre\classes,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\access-bridge-64.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\cldrdata.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\dnsns.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\jaccess.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\jfxrt.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\localedata.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\nashorn.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunec.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunjce_provider.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunmscapi.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\sunpkcs11.jar,G:\Program Files\Java\jdk1.8.0_45\jre\lib\ext\zipfs.jar,.,G:\Program Files\Java\jdk1.8.0_45\lib]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Object.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/String.class)]]
[正在檢查jdk.test.classLoader.Test]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/Serializable.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/AutoCloseable.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Byte.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Character.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Short.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Long.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Float.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Integer.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Double.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Boolean.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Void.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/System.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/PrintStream.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/FilterOutputStream.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/OutputStream.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Comparable.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/CharSequence.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Appendable.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/Closeable.class)]]
[正在加載ZipFileIndexFileObject[G:\Program Files\Java\jdk1.8.0_45\lib\ct.sym(META-INF/sym/rt.jar/java/io/Flushable.class)]]
[已寫(xiě)入RegularFileObject[Test.class]]
[共 1743 毫秒]
$ javap -verbose Test
警告: 二進(jìn)制文件Test包含jdk.test.classLoader.Test
Classfile /D:/workspace/Test/src/jdk/test/classLoader/Test.class
  Last modified 2018-4-14; size 520 bytes
  MD5 checksum 601c8f82d6b3a2ac4ce7c5b04e013d92
  Compiled from "Test.java"
public class jdk.test.classLoader.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #19.#20        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #21            // hello world
   #4 = Methodref          #22.#23        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #24            // jdk/test/classLoader/Test
   #6 = Methodref          #5.#18         // jdk/test/classLoader/Test."<init>":()V
   #7 = Methodref          #5.#25         // jdk/test/classLoader/Test.hello:()V
   #8 = Class              #26            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               hello
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               SourceFile
  #17 = Utf8               Test.java
  #18 = NameAndType        #9:#10         // "<init>":()V
  #19 = Class              #27            // java/lang/System
  #20 = NameAndType        #28:#29        // out:Ljava/io/PrintStream;
  #21 = Utf8               hello world
  #22 = Class              #30            // java/io/PrintStream
  #23 = NameAndType        #31:#32        // println:(Ljava/lang/String;)V
  #24 = Utf8               jdk/test/classLoader/Test
  #25 = NameAndType        #13:#10        // hello:()V
  #26 = Utf8               java/lang/Object
  #27 = Utf8               java/lang/System
  #28 = Utf8               out
  #29 = Utf8               Ljava/io/PrintStream;
  #30 = Utf8               java/io/PrintStream
  #31 = Utf8               println
  #32 = Utf8               (Ljava/lang/String;)V
{
  public jdk.test.classLoader.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0
        line 5: 4

  public void hello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #5                  // class jdk/test/classLoader/Test
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #7                  // Method hello:()V
        12: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 12
}
SourceFile: "Test.java"

初始化

初始化時(shí)類(lèi)加載的最后階段,初始化階段是執(zhí)行類(lèi)構(gòu)造器<clinit>()方法的過(guò)程。
初始化的執(zhí)行時(shí)機(jī),有5種情況會(huì)觸發(fā)初始化:

  1. 遇到new、getstatic、putstatic或invokestatic指令時(shí),如果相關(guān)的類(lèi)沒(méi)有初始化,會(huì)觸發(fā)初始化。場(chǎng)景有new創(chuàng)建對(duì)象,讀寫(xiě)類(lèi)的靜態(tài)變量或調(diào)用類(lèi)的靜態(tài)方法。注意如果是編譯時(shí)期加入常量池的靜態(tài)變量(final static 常量),那么這個(gè)靜態(tài)變量與定義它的類(lèi)已經(jīng)剝離了關(guān)系,這種調(diào)用不會(huì)觸發(fā)該類(lèi)的初始化。
    例如:
public class InitOrder {
    static {
        System.out.println("initOrder init!");
    }

    public static void main(String[] args) {
        System.out.println("before get final static var");
        // Demo.A在編譯時(shí)加入了常量池,是共享的數(shù)據(jù),訪問(wèn)不會(huì)觸發(fā)Demo類(lèi)的初始化
        int a = Demo.A;
        System.out.println("get final static var end");
        System.out.println("before get static var");
        // 非常量的訪問(wèn) 會(huì)觸發(fā)初始化
        int b  = Demo.a;
        System.out.println("get  static var end");
    }
}

class Demo {
    static int a = 100;
    final static int A = 1000;
    static {
        System.out.println("Demo init");
    }
}
// 結(jié)果
// initOrder init!
// before get final static var
// get final static var end
// before get static var
// Demo init
// get  static var end

  1. 使用Java反射機(jī)制的時(shí)候,如果類(lèi)沒(méi)有初始化,會(huì)觸發(fā)初始化。
  2. 初始化一個(gè)類(lèi)時(shí),會(huì)先初始化其父類(lèi)(如果父類(lèi)沒(méi)有初始化的話(huà))
  3. JVM啟動(dòng)時(shí),程序的入口類(lèi)會(huì)先初始化
  4. 使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且這個(gè)句柄對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先進(jìn)行初始化。
    有且僅有以上5種場(chǎng)景會(huì)觸發(fā)初始化。

<clinit>() <init>()

  1. <clinit>()方法是由編譯器自動(dòng)收集類(lèi)中靜態(tài)變量的賦值語(yǔ)句以及靜態(tài)代碼塊中定義的語(yǔ)句合并產(chǎn)生的,且內(nèi)部的語(yǔ)句的順序是由定義的順序決定的,后面的語(yǔ)句可以訪問(wèn)前面定義的變量,反過(guò)來(lái)可以賦值,但是不能訪問(wèn)。
    static{
        i=100;//可以,但是沒(méi)有意義
        System.out.print(i); //不可以
    }
    static int i = 2 ;
    
    public static void main(String[] args) {
        System.out.println(i);  // 2
    }
  1. <clinit>()<init>()是不同的,前者對(duì)應(yīng)的是類(lèi),后者對(duì)應(yīng)的是實(shí)例,即前一個(gè)是Class的構(gòu)造器(是編譯器生成的),后者是實(shí)例對(duì)象的構(gòu)造器(也就是我們定義或繼承的構(gòu)造函數(shù))。且虛擬機(jī)會(huì)保證子類(lèi)的<clinit>()執(zhí)行之前,其父類(lèi)的<clinit>()一定執(zhí)行完成,無(wú)需顯式指定。所以第一個(gè)執(zhí)行<clinit>()的類(lèi)是java.lang.Object;
  2. 因?yàn)楦割?lèi)比子類(lèi)先執(zhí)行<clinit>(),所以父類(lèi)的靜態(tài)變量和靜態(tài)代碼塊是先于子類(lèi)執(zhí)行的。
  3. 如果一個(gè)類(lèi)或者接口中沒(méi)有靜態(tài)變量或靜態(tài)代碼塊,編譯器可以不生成<clinit>()
  4. 接口中沒(méi)有靜態(tài)代碼塊,但是可以有靜態(tài)變量。所以可以有<clinit>()的初始化動(dòng)作,但是接口和類(lèi)不同之處在于接口不需要先執(zhí)行父接口的<clinit>()方法。
  5. 虛擬機(jī)會(huì)保證一個(gè)類(lèi)的<clinit>()方法在多線程環(huán)境中能被正確的同步,利用這一點(diǎn)可以實(shí)現(xiàn)線程安全的單例模式。

初始化結(jié)束后,一個(gè)類(lèi)就可以被正常的使用了。


參考文獻(xiàn)
《深入理解Java虛擬機(jī)》周志明


The end.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容