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

1、概述

虛擬機的類加載機制定義:把描述類的數據從Class文件(一串二進制的字節流)加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成被虛擬機直接使用的Java類型。java

Java語言裏,類型的加載、鏈接和初始化過程都是在程序運行期間完成的,Java裏天生能夠動態擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特色實現的。程序員

用戶能夠經過Java預約義的和自定義類加載器,讓一個本地的應用程序能夠在運行時從網絡或其餘地方加載一個二進制流做爲程序代碼的一部分。數組

2、類加載的時機

2.1 類加載包含那些階段

類從被加載到虛擬機內存中開始,到卸載出內存,所通過的生命週期有:安全

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

其中2-4統稱爲鏈接,上面的過程有幾個須要注意的點:bash

  • 加載、驗證、準備、初始化、卸載這五個階段按順序循序漸進地開始,在一個階段執行的過程當中有可能調用、激活另一個階段。
  • 解析階段有可能在初始化以後開始,這是爲了支持Java語言的運行時綁定

2.2 類加載觸發的時機

有且僅有下面五種狀況必須當即對類進行初始化:網絡

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

2.3 被動引用

2.2中談到的都是主動引用,除此以外,全部引用類的方法都稱爲被動引用,而被動引用不會觸發類的初始化數據結構

  • 類初始化時,若是父類沒有被初始化,那麼會先初始化父類,這一過程將一直遞歸到Object爲止,可是不會去初始化它所實現的接口,即當咱們初始化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");
    }
}
複製代碼
  • 接口初始化時,不要求父接口所有初始化,只有真正用到了父接口的時候(如引用接口中定義的常量),那麼纔會初始化。
  • 當訪問某個類的靜態域時,不會觸發父類的初始化或者子類的初始化,即便靜態域被子類或子接口或者它的實現類所引用,咱們給ClassChild添加一個靜態屬性,訪問這個靜態屬性不會初始化ClassParent
public class ClassChild extends ClassParent {

    public static int sNumber;

    static {
        System.out.println("load ClassChild");
    }
}
複製代碼
  • 若是一個靜態變量是編譯時常量,則對它的引用不會引發定義它的類的初始化,以下面訪問sNumber,那麼不會引發ClassChild的實例化。
public class ClassChild extends ClassParent {

    public static final int sNumber = 2;

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

3、類加載的過程

3.1 加載

在"加載"階段,虛擬機須要完成如下三件事情:多線程

  • 經過一個類的全限定名來獲取定義此類的二進制字節流
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口

3.2 驗證

"驗證"階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害自身的安全,大體會完成下面四個階段的校驗動做:函數

  • 文件格式驗證
  • 元數據驗證
  • 字節碼驗證
  • 符號引用驗證

3.3 準備

"準備"階段是正式爲類變量(被static修飾,而不是實例變量)分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。ui

  • 對於static而且非final的類變量,將被初始化爲數據類型的零值。
  • 對於staticfinal的類變量,在這個階段就會被初始化爲ConstantValue屬性所指定的值。

3.4 解析

「解析」階段是虛擬機將常量池的符號引用替換爲直接引用的過程,包括:

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

3.5 初始化

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

  • <clinit>方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併而成,順序是由語句在源文件中出現的順序決定的。靜態語句塊只能訪問到定義在它以前的變量,對於定義在它後面的變量只能賦值不能訪問。

  • <clinit>()方法與類的構造函數不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢,所以在虛擬機中第一個杯知行的<clinit>()方法的類確定是java.lang.Object

  • 父類的靜態語句塊要優先於子類的變量賦值操做。

  • 若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。

  • 接口不能接口中僅有變量初始化的賦值操做,但執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法,只有當父接口中定義的變量使用時,父接口才會初始化,另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。

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

4、類加載器

4.1 概念

類加載器用來「經過一個類的全限定名來獲取描述此類的二進制字節流」。

4.2 類與類加載器

類加載器用於實現類的加載動做,除此以外,任意一個類,都須要由它加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性。

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

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

4.3 雙親委派模型

絕大部分Java程序都會用到如下三種系統提供的類加載器:

  • 啓動類加載器
  • 擴展類加載器
  • 應用類加載器

類加載器之間的層次關係,稱爲類加載器的雙親委派模型,這個模型要求除了頂層的啓動類加載器外,其他的類都應當有本身的父類加載器,通常使用組合來複用父加載器的代碼。

雙親委派模型的工做過程:若是一個類加載器收到了類加載的請求,它首先不會去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,只有當父類加載器反饋本身沒法完成這個加載請求時,子加載器纔會嘗試本身加載。

5、對象實例化

在類加載過程完畢後,若是須要進行實例化對象就須要通過一下步驟,按優先加載父類,再到子類的順序執行:

  • 加載父類構造器
  • 爲父類實例對象分配存儲空間並賦值
  • 執行父類的初始化塊
  • 執行父類構造函數
  • 加載子類加載器
  • 爲子類實例對象分配存儲控件並賦值
  • 執行子類的初始化塊
  • 執行子類構造函數

咱們用一個簡單的例子: 其中ClassOther是一個單獨的類:

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");
    }
}
複製代碼

當咱們實例化一個ClassChild對象時,調用的順序以下:

相關文章
相關標籤/搜索