Java&Android 基礎(chǔ)知識梳理(5) - 類加載&對象實例化

一、概述

虛擬機(jī)的類加載機(jī)制定義:把描述類的數(shù)據(jù)從Class文件(一串二進(jìn)制的字節(jié)流)加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗、轉(zhuǎn)換解析和初始化,最終形成被虛擬機(jī)直接使用的Java類型。

Java語言里,類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成的,Java里天生可以動態(tài)擴(kuò)展的語言特性就是依賴運(yùn)行期動態(tài)加載和動態(tài)連接這個特點(diǎn)實現(xiàn)的。

用戶可以通過Java預(yù)定義的和自定義類加載器,讓一個本地的應(yīng)用程序可以在運(yùn)行時從網(wǎng)絡(luò)或其他地方加載一個二進(jìn)制流作為程序代碼的一部分。

二、類加載的時機(jī)

2.1 類加載包含那些階段

類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存,所經(jīng)過的生命周期有:

  • 1.加載
  • 2.驗證
  • 3.準(zhǔn)備
  • 4.解析
  • 5.初始化
  • 6.使用
  • 7.卸載

其中2-4統(tǒng)稱為連接,上面的過程有幾個需要注意的點(diǎn):

  • 加載、驗證、準(zhǔn)備、初始化、卸載這五個階段按順序按部就班地開始,在一個階段執(zhí)行的過程中有可能調(diào)用、激活另外一個階段。
  • 解析階段有可能在初始化之后開始,這是為了支持Java語言的運(yùn)行時綁定。

2.2 類加載觸發(fā)的時機(jī)

有且僅有下面五種情況必須立即對類進(jìn)行初始化:

  • 第一種:遇到new/getstatic/putstatic/invokestatic4條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化,場景:
    • 使用new關(guān)鍵字實例化對象
    • 讀取或設(shè)置一個類的靜態(tài)字段(被final修飾,已在編譯期把結(jié)果放入常量池的字段除外)
    • 調(diào)用一個類的靜態(tài)方法
        //1.new關(guān)鍵字.
        LoadInvokeClass loadInvokeClass = new LoadInvokeClass();
        //2.訪問靜態(tài)變量
        int content = LoadInvokeClass.sContent;
        //3.調(diào)用靜態(tài)方法.
        LoadInvokeClass.staticMethod();
  • 第二種:使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
        try {
            Class<?> mClass = Class.forName("com.example.lizejun.repojavalearn.load.LoadInvokeClass");
        } catch (Exception e) { e.printStackTrace(); }
  • 第三種:當(dāng)初始化一個類的時候,如果需要初始化其父類,但是發(fā)現(xiàn)父類沒有初始化、那么需要先觸發(fā)其父類的初始化。
        //其中LoadInvokeClass是LoadInvokeClassChild的父類.
        LoadInvokeClassChild classChild = new LoadInvokeClassChild();
  • 第四種:當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法),虛擬機(jī)會先初始化這個主類。
  • 第五種:使用JDK 1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic/REF_putStatic/REF_invokeStatic的句柄方法,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。

2.3 被動引用

2.2中談到的都是主動引用,除此之外,所有引用類的方法都稱為被動引用,而被動引用不會觸發(fā)類的初始化

  • 類初始化時,如果父類沒有被初始化,那么會先初始化父類,這一過程將一直遞歸到Object為止,但是不會去初始化它所實現(xiàn)的接口,即當(dāng)我們初始化ClassChild的時候,只會先初始化ClassParent,但不會初始化ClassInterface。
public interface ClassInterface {}

public class ClassParent implements ClassInterface {
    static {
        System.out.println("load ClassParent");
    }
}

public class ClassChild extends ClassParent {
    static {
        System.out.println("load ClassChild");
    }
}
  • 接口初始化時,不要求父接口全部初始化,只有真正用到了父接口的時候(如引用接口中定義的常量),那么才會初始化。
  • 當(dāng)訪問某個類的靜態(tài)域時,不會觸發(fā)父類的初始化或者子類的初始化,即使靜態(tài)域被子類或子接口或者它的實現(xiàn)類所引用,我們給ClassChild添加一個靜態(tài)屬性,訪問這個靜態(tài)屬性不會初始化ClassParent。
public class ClassChild extends ClassParent {

    public static int sNumber;

    static {
        System.out.println("load ClassChild");
    }
}
  • 如果一個靜態(tài)變量是編譯時常量,則對它的引用不會引起定義它的類的初始化,如下面訪問sNumber,那么不會引起ClassChild的實例化。
public class ClassChild extends ClassParent {

    public static final int sNumber = 2;

    static {
        System.out.println("load ClassChild");
    }
}
  • 通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化。
ClassChild[] children = new ClassChild[10];

三、類加載的過程

3.1 加載

在"加載"階段,虛擬機(jī)需要完成以下三件事情:

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

3.2 驗證

"驗證"階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害自身的安全,大致會完成下面四個階段的校驗動作:

  • 文件格式驗證
  • 元數(shù)據(jù)驗證
  • 字節(jié)碼驗證
  • 符號引用驗證

3.3 準(zhǔn)備

"準(zhǔn)備"階段是正式為類變量(被static修飾,而不是實例變量)分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。

  • 對于static并且非final的類變量,將被初始化為數(shù)據(jù)類型的零值。
  • 對于staticfinal的類變量,在這個階段就會被初始化為ConstantValue屬性所指定的值。

3.4 解析

“解析”階段是虛擬機(jī)將常量池的符號引用替換為直接引用的過程,包括:

  • 類或接口的解析
  • 字段解析
  • 類方法解析
  • 接口方法解析

3.5 初始化

根據(jù)程序員通過程序指定的主觀計劃去初始化類變量和其它資源,也就是執(zhí)行類構(gòu)造器<clinit>()方法的過程:

  • <clinit>方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并而成,順序是由語句在源文件中出現(xiàn)的順序決定的。靜態(tài)語句塊只能訪問到定義在它之前的變量,對于定義在它后面的變量只能賦值不能訪問。

  • <clinit>()方法與類的構(gòu)造函數(shù)不同,它不需要顯示地調(diào)用父類構(gòu)造器,虛擬機(jī)會保證在子類的<clinit>()方法執(zhí)行前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢,因此在虛擬機(jī)中第一個杯知行的<clinit>()方法的類肯定是java.lang.Object

  • 父類的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。

  • 如果一個類中沒有靜態(tài)語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。

  • 接口不能接口中僅有變量初始化的賦值操作,但執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法,只有當(dāng)父接口中定義的變量使用時,父接口才會初始化,另外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法。

  • 虛擬機(jī)會保證一個類的<clinit>()方法在多線程環(huán)境中被正確地加鎖、同步。

四、類加載器

4.1 概念

類加載器用來“通過一個類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流”。

4.2 類與類加載器

類加載器用于實現(xiàn)類的加載動作,除此之外,任意一個類,都需要由它加載它的類加載器和這個類本身一同確立其在Java虛擬機(jī)中的唯一性。

每一個類加載器,都擁有一個獨(dú)立的類名稱空間,比較兩個類是否相等,只有在兩個類由同一個類加載器加載的前提下才有意義。

相等代表類的Class對象的equals方法,isAssignableFrom方法,isInstance方法。

4.3 雙親委派模型

絕大部分Java程序都會用到以下三種系統(tǒng)提供的類加載器:

  • 啟動類加載器
  • 擴(kuò)展類加載器
  • 應(yīng)用類加載器

類加載器之間的層次關(guān)系,稱為類加載器的雙親委派模型,這個模型要求除了頂層的啟動類加載器外,其余的類都應(yīng)當(dāng)有自己的父類加載器,一般使用組合來復(fù)用父加載器的代碼。

雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,只有當(dāng)父類加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己加載。

五、對象實例化

在類加載過程完畢后,如果需要進(jìn)行實例化對象就需要經(jīng)過一下步驟,按優(yōu)先加載父類,再到子類的順序執(zhí)行:

  • 加載父類構(gòu)造器
  • 為父類實例對象分配存儲空間并賦值
  • 執(zhí)行父類的初始化塊
  • 執(zhí)行父類構(gòu)造函數(shù)
  • 加載子類加載器
  • 為子類實例對象分配存儲控件并賦值
  • 執(zhí)行子類的初始化塊
  • 執(zhí)行子類構(gòu)造函數(shù)

我們用一個簡單的例子:
其中ClassOther是一個單獨(dú)的類:

public class ClassOther {

    public int mNumber;

    public ClassOther() {
        System.out.println("ClassOther Constructor");
    }

    public void setNumber(int number) {
        this.mNumber = number;
    }

    public int getNumber() {
        return mNumber;
    }
}

ClassChild則繼承于ClassChild

public class ClassParent {

    {
        System.out.println("ClassParent before mClassParentContent");
    }

    private ClassOther mClassParentContent = new ClassOther(10);

    {
        System.out.println("ClassParent after mClassParentContent=" + mClassParentContent.mNumber);
    }

    public ClassParent(int number) {
        mClassParentContent.setNumber(number);
        System.out.println("ClassParent Constructor, mClassParentContent=" + mClassParentContent.mNumber);
    }


}

public class ClassChild extends ClassParent {

    {
        System.out.println("ClassChild before a");
    }

    private int mClassChildContent = 1;

    {
        System.out.println("ClassChild after mClassChildContent=" + mClassChildContent);
    }

    public ClassChild() {
        super(2);
        System.out.println("ClassChild Constructor");
    }
}

當(dāng)我們實例化一個ClassChild對象時,調(diào)用的順序如下:

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

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

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