好程式設計師Java學習路線分享JVM類載入機制

好程式設計師IT發表於2019-09-19

好程式設計師Java 學習路線分享 JVM 類載入機制 JVM 相關概念

- jdk<br>

    jdk Java Development Kit Java 開發包,是 Java 開發人員用於編譯和除錯程式的一套程式的集合。

- jre<br>

    jre Java Runtime Evironment Java 執行時環境,是執行 Java 程式的平臺,所有的 Java 程式必須在這個平臺中才能執行。

- jvm<br>

jvm Java Virtual Machine Java 虛擬機器,是用程式碼虛擬出來的計算機,模擬執行計算機的各項功能,它有自己的硬體架構,如:處理器、堆疊、暫存器等,還有自己的一套指令系統,在不同的作業系統上都可以安裝 JVM ,從而實現 Java 程式在不同的作業系統上都能執行, JVM 就是為實現 Java 的跨平臺特性。

JVM 載入類的過程

我們執行Java 程式開發出來後,需要先編譯再執行, JVM 就負責載入類的過程。 <br>

類載入的過程分為:

1. 載入

2. 驗證

3. 準備

4. 解析

5. 初始化

類載入的具體過程

下面詳細介紹下這幾個過程:

1. 載入 <br>

     在載入類的過程要完成:

    1. 根據類的全名限定符,獲取 class 二進位制流,這個流可以從磁碟上的 class jar 檔案獲得,也可以從網路中獲得。

    2. 將類的靜態儲存結構轉化為方法區的執行時動態儲存結構

    3. 在記憶體的堆中生成對應的 java.lang.Class 物件,作為方法區的入口

2. 驗證 <br>

     載入類完成後,就進入了驗證過程,這個過程保證了前面生成的Class 物件中的資訊,不會危害 JVM 的安全。 <br>

     需要驗證的方面有:

    1. 檔案格式驗證,是要驗證位元組流是否符合 Class 檔案格式的規範,並且能被當前版本的虛擬機器處理。如驗證魔數是否 0xCAFEBABE ;主、次版本號是否正在當前虛擬機器處理範圍之內;常量池的常量中是否有不被支援的常量型別等等,該驗證階段的主要目的是保證輸入的位元組流能正確地解析並儲存於方法區中,經過這個階段的驗證後,位元組流才會進入記憶體的方法區中儲存,所以後面的三個驗證階段都是基於方法區的儲存結構進行的。

    2. 後設資料驗證,是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合 Java 語言規範的要求。可能包括的驗證如:這個類是否有父類;這個類的父類是否繼承了不允許被繼承的類;如果這個類不是抽象類,是否實現了其父類或介面中要求實現的所有方法。

    3. 位元組碼驗證,主要工作是進行資料流和控制流分析,保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為。如果一個類方法體的位元組碼沒有透過位元組碼驗證,那肯定是有問題的;但如果一個方法體透過了位元組碼驗證,也不能說明其一定就是安全的。

    4. 符號引用驗證,發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在“解析階段”中發生。驗證符號引用中透過字串描述的許可權定名是否能找到對應的類;在指定類中是否存在符合方法欄位的描述符及簡單名稱所描述的方法和欄位;符號引用中的類、欄位和方法的訪問性 (private protected public default) 是否可被當前類訪問。

3. 準備 <br>

     準備階段會在方法區中為類的靜態變數分配記憶體,並賦給預設值。

    ```

    public static int count = 100;

    ```

     如:上面的count 變數在準備階段會賦值為 0 ,在初始化時再賦值為 100;

4. 解析 <br>

     解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

    - 符號引用( Symbolic Reference <br>

         符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。

    - 直接引用( Direct Reference <br>

         直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是與虛擬機器實現的記憶體佈局相關的,如果有了直接引用,那麼引用的目標必定已經在記憶體中存在。

5. 初始化 <br>

     類初始化是類載入過程的最後一步,前面的類載入過程,除了在載入階段使用者應用程式可以透過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的Java 程式程式碼。 <br>

     初始化階段是執行類構造器<clinit>() 方法的過程。 <clinit>() 方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊 (static{} ) 中的語句合併產生的。

     那麼何時執行初始化呢?

    1. 建立類的例項

    2. 訪問類的靜態變數 ( 除常量外, final 修飾的 )

         原因: 常量一種特殊的變數,因為編譯器把他們當作值而不是屬性來對待。

    3. 訪問類的靜態方法

    4. 反射如 (Class.forName("com.test.Person"))

    5. 當初始化一個類時,發現其父類還未初始化,則先呼叫父類的初始化

    6. 虛擬機器啟動時,定義了 main() 方法的那個類先初始化

程式碼案例

瞭解了類的載入機制,我們來看一道面試題:

```

public class MySingleton {

        private static MySingleton singleton = new MySingleton();

        public static int count1 = 0;

        public static int count2;

       

        private MySingleton(){

                count1++;

                count2++;

        }

       

        public static MySingleton getInstance(){

                return singleton;

        }

       

        public static void main(String[] args) {

                MySingleton singleton = MySingleton.getInstance();

                System.out.println("count1-->"+MySingleton.count1);

                System.out.println("count2-->"+MySingleton.count2);

        }

}

上面的結果,大多數同學可能認為兩個靜態變數都是1 ,結果比較意外:

```

count1-->0

count2-->1

```

這是為什麼呢?下面我們來分析下:

1. 首先我們知道在類的準備階段會為靜態變數賦預設值: <br>

singleton = null;

count1 = 0;

count2 = 0;

2. 當呼叫類的靜態方法 getInstance 後,引發類的初始化,先執行 new MySingleton() 呼叫構造方法,這時: <br>

count1 = 1;

count2 = 1;

3. 繼續初始化,為變數賦值, count1 賦值為 0 count2 沒有賦值就保留值 1 ,結果就是 :<br>

count1 = 0;

count2 = 1;

總結

JVM 是程式碼模擬的計算機,有自己的硬體和軟體, JVM 能實現 Java 類的載入和執行,具體載入過程有:載入、驗證、準備、解析、初始化 5 個步驟組成。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69913892/viewspace-2657509/,如需轉載,請註明出處,否則將追究法律責任。

相關文章