JVM初探(三):類加載機制

1、概述

咱們知道java代碼會被編譯爲.class文件,這裏class文件中的類信息最終仍是須要jvm加載之後才能使用。html

事實上,虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型的過程就是虛擬機的類加載機制java

對於jvm類的加載機制,咱們主要關注兩個問題:程序員

  • 類的加載時機?(初始化的五種狀況)
  • 類的加載過程?(類的五個加載過程)

2、類的加載時機

1.類的生命週期

類從被加載到虛擬機內存中開始,到卸載出內存爲止,整個生命週期包括加載、驗證、準備、解析、初始化、使用和卸載。其中驗證、準備、解析統稱爲鏈接數組

類加載的時機

值得一提的是,加載,驗證,準備,初始化和卸載是固定的,可是解析階段不是:它在必定狀況下能夠在初始化後再開始,以支持java語言的動態綁定安全

這裏解釋一下動態綁定和靜態綁定:數據結構

靜態綁定:
在程序執行前方法已經被綁定(也就是說在編譯過程當中就已經知道這個方法究竟是哪一個類中的方法),此時由編譯器或其它鏈接程序實現。多線程

動態綁定:
後期綁定:在運行時根據具體對象的類型進行綁定。jvm

另外,類的加載過程必須按步驟「開始」,可是並不等前一個步驟完成後才進行下一個步驟,而是在前一個步驟進行時就開始下一個步驟。佈局

2.類的加載時機

這裏的「加載」只是類加載過程的一個階段,表明這「類的加載」的這一過程的開始,jvm並無強制性約束在何時開始類加載過程性能

通常咱們說類的加載,指的是整個加載過程。過程完成後,表明jvm將java文件編譯成class文件後,以二進制流的方式存放到運行時數據的方法區中,並在java的堆中建立一個java.lang.Class對象,用來指向存放在方法堆中的數據結構。

3.類的初始化時機

首先咱們得明確一下初始化和實例化的區別:

類的實例化是指建立一個類的實例(對象)的過程;

類的初始化是指爲類中各個類成員(被static修飾的成員變量)賦初始值的過程,是類生命週期中的一個階段。

初始化通常是類使用前的最後一個階段,因此類初始化時機能夠當作類的加載時機。

凡有如下四種行爲的成爲對一個類進行主動引用只有主動引用會觸發類的初始化

  • 遇到四條字節碼指令
    1. new:使用new關鍵字實例化對象;
    2. getstatic:獲取一個不被final修飾的類的靜態字段;
    3. putstatic:設置一個不被final修飾的類的靜態字段;
    4. invokestatic:調用一個類的靜態方法;
  • 使用java.lang.reflect包中的方法對類進行反射調用時,若是類尚未初始化,則必須首先對其初始化;
  • 當初始化一個類時,若是其父類尚未初始化,則必須首先初始化其父類;
  • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  • 當使用JDK7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則須要先觸發其初始化。

除了以上五種方式之外引用類的方式成爲被動引用,並不會觸發初始化。

被動引用有如下幾種表明性的例子:

假設咱們有如下兩種類:

/**
 * @Author:CreateSequence
 * @Date:2020-08-08 21:28
 * @Description:Parent類
 */
public class Parent {
    
    static int ParentAge = 10;
    
    static {
        System.out.println("我是Parent,我被初始化了!");
    }
}

/**
 * @Author:CreateSequence
 * @Date:2020-08-08 21:28
 * @Description:Child類
 */
public class Child extends Parent {

    public static final int cons = 55;

    static {
        System.out.println("我是Child,我被初始化了!");
    }
}
  • 經過子類引用父類的靜態字段,不會致使子類初始化;

    public static void main( String[] args ) {
        System.out.println(Child.ParentAge);
    }
    
    //輸出
    我是Parent,我被初始化了!
    10
  • 經過數組定義引用類不會初始化;

    public static void main( String[] args ) {
        Parent[] Parent = new Parent[10];
    }
  • 常量在編譯階段會存入調用類的常量池中,本質上並無引用到定義常量的類,所以不會觸發定義常量的類的初始化

    public static void main( String[] args ) {
        System.out.println(Child.cons);
    }
    
    //輸出
    55

3、類的加載過程

1.加載

加載」是由類加載器完成的「類加載過程」的第一個階段,在初始化以前完成。

加載階段完成如下三件事:

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

值得一提的是,二進制流能夠從zip包中獲取,這也是JAR或者WAR包格式也能部署項目基礎。

另外,類的加載階段涉及類加載器和雙親委派模型等知識點,此處將另起新隨筆詳細介紹,在本文就很少費筆墨了。

2.驗證

驗證是鏈接階段的第一步,目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。

驗證階段完成如下四件事:

  • 文件格式驗證:驗證字節流是否符合Class文件格式的規範,

    好比是否以魔數0xCAFEBABE開頭、主次版本號是否在當前虛擬機處理範圍內、常量池的常量中是否有不被支持的常量類型等等;

  • 元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,

    好比父類是否繼承了被final修飾的類,非抽象類是否都實現了父類或者接口的方法等等;

  • 字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的;

  • 符號引用驗證:對類自身之外的信息進行匹配性校驗,

    好比符號引用中經過字符串描述的全限定名是否能找到對應的類等等。

3.準備

準備階段是正式爲類被static修飾的變量(不包含實例變量)分配內存並設置類變量初始值的階段。

這裏區分常量與普通靜態變量:

對於普通靜態變量,好比 public staic int num = 1,準備階段賦值爲0,而把value賦值爲123的putstatic指令是程序被編譯後,存放於虛擬機裝載一個類初始化的時候調用的類構造器方法<clinit>()之中,因此把value賦值爲123的動做將在初始化階段纔會執行。

而對於常量類型,好比 public static final int = 1,準備階段就會賦值爲1。

4.解析

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

這裏咱們須要理解一下符號引用和直接引用:

  • 符號引用:以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能夠無歧義的定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用目標並不必定已經加載到內存中
  • 直接引用:直接指向目標的指針、相對偏移量或一個能間接定位到目標的句柄,直接引用與虛擬機實現的內存佈局相關,若是有了直接引用,引用目標一定已經加載到內存中

咱們舉個簡單的例子:

最開始jvm要加載People類,可是一開始並不知道People的內存地址,此時就用符號「People」先表示它的地址,等到類加載器加載完People類的時候,就能夠知道People類的實際地址了,因而就將「People」符號換成People這個類的實際內存地址。

5.初始化

類初始化階段是類加載過程的最後一步。在前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的java程序代碼(字節碼)。

這裏咱們能夠回頭看準備階段,咱們知道準備階段會調用類構造器<clinit>()方法.

實際上,初始化階段就是執行類構造器<clinit>()方法的過程。

4、初始化時的類構造器

咱們在類加載的驗證和初始化時都提到過類構造器 <clinit>(),這裏稍微介紹一下。

<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做靜態語句塊中的語句合併產生的。也就是說,若是一個類沒有靜態成員變量和靜態塊,是能夠不執行類構造方法的。

1.父類子類類構造器的執行順序

類構造器<clinit>()與實例構造器<init>()不一樣,它不須要程序員進行顯式調用,虛擬機會保證在子類類構造器<clinit>()執行以前,父類的類構造<clinit>()執行完畢。這就致使了父類靜態代碼塊比子類靜態代碼塊先執行

2.類構造器中的賦值操做

對於靜態塊中的賦值操做,咱們須要注意:靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問

舉個例子:

static {
    i = 5;
    System.out.println(i);//在此處拋出錯誤:非法的向前引用
}

public static int i = 0;

3.多線程環境下的類構造器

在多線程環境下,虛擬機會保證老是隻有一個線程去執行類構造器 <clinit>(),其餘線程會阻塞直到構造器執行完畢。而一個類只會進行一次初始化,這就保證了多線程下類的正確初始化。

事實上,這有點像在我關於多線程的這篇文章中提到的雙重檢查單例模式,也是由於這點,咱們能夠巧妙的使用內部類來實現一個線程安全的單例模式。

因爲實例化的時候其餘線程會阻塞,因此若是在類的靜態塊中進行了耗時較長的工做時,可能就會致使多個線程在你不知道的狀況下堵塞,形成沒必要要的性能消耗。

相關文章
相關標籤/搜索