1. 程式人生 > >【JVM】類載入、連線和初始化過程

【JVM】類載入、連線和初始化過程

程式執行時,載入類主要經過3個階段分別是類的載入,連線和初始化。分別介紹一下這三個過程。

一、載入

類的載入指的是將類的.class檔案中二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個
java.lang.Class物件,用來封裝類在方法區內的資料結構。在這個階段,會執行類中宣告的靜態程式碼塊。也就是類中的靜態塊執行時不需要等到類的初始化。

載入.class檔案的方式

1、從本地系統中直接載入
2、通過網路下載.class檔案
3、從zip,jar等歸檔檔案中載入.class檔案
4、從專有資料庫中提取.class檔案
5、將Java原始檔動態編譯為.class檔案

類載入的最終產品是位於堆區中的class物件,Class物件封裝了類在方法區內的資料結構,並向Java程式設計師提供了訪問方法區內的資料機構的介面.
我們可以通過類名.class來獲取一個類的型別的引用,通過new 類名().getClass()來獲取一個例項變數的類的引用

類的載入機制
從JDK1.2開始類載入採用父親委託機制。除了Java虛擬機器自帶的根類載入器以外,其餘的類載入器都有且只有一個父載入器。當Java程式騎牛載入器載入某個類時,載入器會首先委託自己的父載入器去載入該類,若父載入器能載入,則由父載入器完成載入任務,否則才由自載入器去載入。
這裡寫圖片描述
同時,所有能成功返回Class物件的引用的類載入器(包括定義類載入器,即包括定義類載入器和它下面的所有子載入器)都被稱為初始類載入器。
假設loader1實際載入了Sample類,則loader1為Sample類的定義類載入器

二、連線

類載入完成後就進入了類的連線階段,連線階段主要分為三個過程分別是:驗證,準備和解析。在連線階段,主要是將已經讀到記憶體的類的二進位制資料合併到虛擬機器的執行時環境中去。
驗證

這個階段主要目的是保證Class流的格式是正確的。主要驗證的內容包括:
1、檔案格式的驗證
    是否以0xCAFEBABE開頭
    版本號是否合理
2、元資料的驗證
    是否有父類
    是否繼承了final類
    非抽象類實現了所有抽象方法
3、位元組碼驗證
    執行檢查
    棧資料型別和操作碼資料引數吻合
    跳轉指令指定到合理的位置
4、符號引用驗證
    常量池中描述類是否存在
    訪問的方法或欄位是否存在且有足夠的許可權

準備

這個階段主要是為物件和變數分配記憶體,併為類設定初始值(方法區中)

對於static型別變數在這個階段會為其賦值為預設值,比如public static int v=5,在這個階段會為其賦值為v=0,而對於static final型別的變數,在準備階段就會被賦值為正確的值
解析
在這個階段會將符號引用轉換成直接引用。
原來的符號引用僅僅是一個字串,而引用的物件不一定被載入,直接引用只的是將引用物件的指標或者地址偏移量指向真正的物件,將字串所指向的物件載入到記憶體中。

三、初始化

在這個階段主要執行類的構造方法。並且為靜態變數賦值為初始值,執行靜態塊程式碼。

類的初始化步驟
1、假如這個類還沒有被載入和連線,那就先進行載入和連線
2、假如類存在直接的父類,並且這個父類還沒有被初始化,那就先初始化它的父類
3、假如類中存在初始化語句時,那就依次執行這些初始化語句。
類的初始化時機
所有的Java類只有在對類的首次主動使用時才會被初始化。主動使用的情況有六中,其他情況都屬於被動使用:
1、 建立類的例項
2、訪問某個類或介面的靜態變數,或者對該靜態變數賦值
3、呼叫類的靜態方法
4、反射(Class.fotName)
5、初始化一個類的子類
6、Java虛擬機器啟動時被標明為啟動類的類(面方法所在的類)
注意:1、當Java虛擬機器初始化一個類時,要求他的所有父類都已經被初始化,但是這條規則並不適合介面。在初始化一個類或介面時,並不會先初始化它所實現的介面。
2、只有當程式訪問的靜態變數或靜態方法確實在當前類或當前介面中定義時,才可以認為是對類或介面的主動使用。如果靜態方法或變數在parent中定義,從子類進行呼叫,則不會初始化子類。

四、實戰分析

1、

public class testStatic {
    public static void main(String args[]){
        Singleton singleton=Singleton.getInstance();
        System.out.println(singleton.count1);
        System.out.println(singleton.count2);
    }
}

class Singleton{
    private static Singleton singleton=new Singleton();
    public static int count1;
    public static int count2=0;

    private Singleton(){
        count1++;
        count2++;
    }

    public static Singleton getInstance(){
        return singleton;
    }
}

檢視輸出結果:
這裡寫圖片描述
但是如果我將構造方法和賦值語句調換位置如下:

class Singleton{

    public static int count1;
    public static int count2=0;
     private static Singleton singleton=new Singleton();//調換了位置
    private Singleton(){
        count1++;
        count2++;
    }

    public static Singleton getInstance(){
        return singleton;
    }
}

結果如下:
這裡寫圖片描述
分析原因:1、在main方法中呼叫單利模式建立類的例項時,是對類的主動使用,同時在類的初始化時,會執行類的構造方法,在第一種情況下,執行完構造方法時,
Java虛擬機器會對類中的靜態變數進行復制,所以按順序執行count1沒有被賦值,還是count1++=1,而count2=0所以又重新被賦值為0了。
2、而第二種情況,是先賦值,再執行構造方法,所以結果為1,1.這個小例子說明類在初始化時,類裡面的靜態變數賦值語句和構造方法執行時是有先後順序的。

2、第二個小例子

class FinalTest {
   // public static final int x=new Random().nextInt(100); //需要在執行時賦值,所以需要進行初始化
   public static final int x=2; //在編譯時已經確定,不需要進行初始化
    static
    {
        System.out.println("static block");
    }
}
public class testfinal{
    static{
        System.out.println("作為啟動類測試");
    }
    public static void main(String args[]){
        System.out.println(FinalTest.x);
    }
}

在兩種情況宣告x,產生的結果是不一樣的。
public static final int x=new Random().nextInt(100);
結果:
這裡寫圖片描述

public static final int x=2
結果:
這裡寫圖片描述
分析原因:
1、兩種情況都輸出了”作為啟動類測試”,這是因為這個靜態塊放在了main函式所在的類作為了啟動類,所以這屬於對類的主動使用,所以每次都會執行這個靜態塊。
2、第一種情況輸出了”static block”是因為在宣告x時是new Random.nextInt(100),而這句宣告在編譯時是不能確定x的具體的值的。所以在執行時,需要去計算x的值,這就相當於呼叫了類中的靜態變數。所以會去初始化該類。
3、第二種情況沒有輸出”static block”是因為x=2,而這條語句編譯器在編譯時就能確定x的值,所以在執行時直接呼叫它的值就可以了,不需要去初始化這個類。所以不會去執行靜態塊。
以上就是Java類從載入,連線到初始化的整個過程。