Java基礎篇—Java類加載機制

Java 類加載機制

Java類加載過程

基於 JDK8,面試常見題型,能夠先試試下面的牛刀小試面試題,而後再通讀全文,效果更佳~java

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括面試

  • 加載 Loading
  • 連接 Linking
    • 驗證 Verification
    • 準備 Preparation
    • 解析 Resolution
  • 初始化 Initialization
  • 使用 Using
  • 卸載 Unloading

加載驗證準備初始化卸載 這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而後一般互相交叉地混合式進行,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。編程

加載

加載的過程當中主要作了3件事:bootstrap

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

對於一個類或者接口數組

  • 若是該類不是數組類,則使用類加載器加載二進制表示。
  • 若是該類是數組類,則由Java虛擬機建立,由於數組類不具備外部二進制表示形式。
類加載器種類

有兩種類型的類加載器:安全

  • 由 JVM 提供的 bootstrap 類加載器:用於加載 系統變量 sun.boot.class.path 所表明的路徑下的 class 文件,頂層父類網絡

  • 用戶定義的類加載器(java 類庫中定義的也包括在內)數據結構

    • ExtClassLoader:sun.misc.Launcher 類中定義,用於加載 系統變量 java.class.path 所表明的路徑下的 class 文件;父類加載器爲 bootstrap
    • AppClassLoader:sun.misc.Launcher 類中定義,用於加載 系統變量 java.ext.dirs 所表明的 路徑下 class 文件;父類加載器爲 ExtClassLoader
    • 其餘,固然你也能夠定義其餘類型的類加載器
類加載器加載原理

ClassLoader 使用 雙親委派模型 來加載類,每一個 ClassLoader 實例都持有父類加載器的引用,虛擬機內置的 bootstrap 類加載器爲頂層父類加載器,沒有父類加載器,但能夠做爲其它 ClassLoader 實例的父類加載器。當 ClassLoader 實例須要加載某個類時,它會先委派其父類加載器去加載。這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,若是沒加載到,則把任務轉交給Extension ClassLoader試圖加載,若是也沒加載到,則轉交給App ClassLoader 進行加載,若是它也沒有加載獲得的話,則返回給委託的發起者,由它到指定的文件系統或網絡等URL中加載該類。若是它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。不然將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。oracle

雙親委派機制的優勢是能夠避免類的重複加載,當父類加載了子類就不必再加載。另外可以保證虛擬機的安全,防止內部實現類被自定義的類替換。函數

那麼JVM在搜索類的時候,如何判斷兩個 class 是否相同呢?答案是不只全類名要相同 ,並且還要由同一個類加載器實例加載。

驗證

主要確保類或接口的二進制表示在結構上是正確的。

準備

準備工做包括爲類或接口建立靜態字段,並將這些字段初始化爲其默認值,這裏 不須要執行任何 java 代碼。

例如,對於類或接口中的以下靜態字段

private static int num = 666;
複製代碼

在準備階段會爲 num 設置默認值 0;在後面的初始化階段纔會給 num 賦值 666;

特殊狀況:對於同時被 static 和 final 修飾的 字段,準備階段就會賦值。

解析

解析是將運行常量池中的符號引用 動態肯定爲具體值的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

初始化

初始化階段是初始化類變量和其餘資源,或者說是執行類構造器<clinit>()方法的過程.

<clinit>()方法是由編譯器自動收集類中的全部==類變量==(static 修飾的變量)的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。

例如:非法向前引用變量示例

static {
    i = 2;
    System.out.println(i); //illegal forward reference
}
static int i = 4;
複製代碼

<clinit>()方法與類的構造函數(或者說實例構造器<init>()方法)不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中第一個被執行的<clinit>()方法的類確定是 java.lang.Object。接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法,只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。

<clinit>()方法是類構造器,在類初始化的時候執行,用於 類中的靜態代碼塊 和 靜態字段 初始化的方法,只會執行一次。

<init>()方法是 類實例的構造器,在對象的初始化階段執行,用於非靜態字段,非靜態代碼塊,構造函數的初始化,能夠執行屢次。

類或者接口只能因爲如下緣由初始化:

  1. 使用 new 建立新對象,若是引用的類還沒有被初始化,則初始化該類;或者從類中獲取 靜態字段、設置靜態字段、執行類中的靜態方法時,若是尚未被初始化,則 聲明該字段或者方法的類或者接口被初始化
  2. 第一次調用 java.lang.invoke.method handle實例,調用了靜態方法,或者 new 的方法的句柄。
  3. 使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化
  4. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化
  5. 當初始化一個類,而該類直接或者間接 實現了 一個 不含有抽象方法,和靜態方法的接口,則須要初始化該接口。
  6. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

牛刀小試1

執行以下代碼,輸出如何:

public class SSClass {
    static
    {
        System.out.println("SSClass");
    }
}    
public class SuperClass extends SSClass {
    static
    {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;

    public SuperClass() {
        System.out.println("init SuperClass");
    }
}
public class SubClass extends SuperClass {
    static 
    {
        System.out.println("SubClass init");
    }

    static int a;

    public SubClass() {
        System.out.println("init SubClass");
    }
}
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

複製代碼

輸出:

SSClass
SuperClass init!
123
複製代碼

解析:

上面提到了這樣一句話:

從類中獲取 靜態字段、設置靜態字段、執行類中的靜態方法時,若是尚未被初始化,則 聲明該字段或者方法的類或者接口被初始化

因此 SubClass 類並不會被初始化,因此也就不會執行其 靜態代碼塊;

牛刀小試2

public class StaticTest {

    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest() {
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {
        System.out.println("4");
    }

    int a = 110;
    static int b = 112;
}

複製代碼

問題:執行上面的程序,輸出結果是什麼?

答案:執行以上代碼的輸出結果:

2
3
a=110,b=0
1
4
複製代碼

解析:

  • 執行 main 方法,會致使主類 StaticTest 初始化,因爲還未加載,先執行加載,主要分析 準備階段 和 初始化階段 的 賦值操做。

  • 準備階段 : 爲靜態字段賦初始值,st 設爲 null ,b 設爲 0;

  • 初始化階段:執行 Java 代碼的類構造器 <clinit>()方法,分別按順序執行以下代碼:

    • static StaticTest st = new StaticTest();new 會 致使 StaticTest 類執行類初始化,執行 <init>()方法,對象的初始化是先初始化成員變量 和 代碼塊,再載執行構造方法;
      • int a = 110;
      • System.out.println("2");
      • System.out.println("3");
      • System.out.println("a=" + a + ",b=" + b);
    • static 靜態代碼塊,System.out.println("1");
    • static int b = 112;
  • 調用 staticFunction() 方法,System.out.println("4");

稍微修改一下代碼,去除 如下代碼,或許就變得正常多了,

static StaticTest st = new StaticTest();
複製代碼

輸出:

1
4
複製代碼

減小了 類實例化的步驟。

父類和子類的初始化順序能夠簡單用如下幾句話歸納:

  • 父類的靜態變量賦值
  • 自身的靜態變量賦值
  • 父類成員變量賦值和父類塊賦值
  • 父類構造函數賦值
  • 自身成員變量賦值和自身塊賦值
  • 自身構造函數賦值

參考:

深刻理解Java虛擬機

oracle 官方文檔

歡迎關注編程那點事兒,隨時隨地,想學就學,掃碼關注吧~

相關文章
相關標籤/搜索