JVM(複習)類加載機制

JVM(複習)類加載機制

類從被加載到虛擬機內存開始,到卸載出內存爲止,整個生命週期包括:加載,驗證,準備,解析,初始化,使用和卸載。

一,類加載階段

《深入理解java虛擬機》中這樣描述類的加載階段

在加載階段,虛擬機需要完成以下三件事:

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

我是這麼理解的:

在這個階段,類通過類加載器通過類的全限定類名,將類的二進制字節流轉爲方法區運行時的數據結構,這個數據結構在c++中是用instanceKlass描述,即相當於用instanceKlass來描述這個java類,這個instanceKlass的重要字段有:

  • _java_mirror:java類的鏡像,例如對String來說,鏡像就是String.class,作用就是把instanceKlass暴露給java訪問其他下面的字段(java不能直接訪問instanceKlass)
  • _super:即父類
  • _fields:成員變量
  • _methods:方法
  • _constants:常量池
  • _class_loader:類加載器
  • _vtable:虛方法表
  • _itable:接口方法表

instanceKlass在jdk1.7是存儲在方法區中,1.8後存儲在元空間

_java_mirror是存儲在堆中

對象實例化過程

現在我要newPerson類的兩個對象

Person p1 = new Person("張三",18);
Person p2 = new Person("李四",20);

類加載器完成Person類的加載後,元空間中存儲該類在方法區中的數據結構instanceKlass,其中鏡像_java_mirror指向在堆中創建的Person.class對象,這個對象作爲訪問元空間中各種數據類型的入口,每個Person實例的對象頭都持有指向該鏡像的指針(class pointer) JVM通過這個指針確定對象是哪個類的實例

在這裏插入圖片描述

對於數組類:

數組本身不通過類加載器創建,他是由java虛擬機直接創建的,但是數組類的元素類型最終也是靠類加載器去創建,

二,連接階段

連接包括三個階段:

  • 驗證
  • 準備
  • 解析

2.1驗證

驗證是連接的第一步,這一階段是爲了確保Class文件的字節流包含的信息符合java虛擬機的要求,並且不會危害java虛擬機自身的安全,如果驗證到輸入的字節流不符合Class文件格式的約束,虛擬機會拋出一個Java.lang.VerifyError異常或其子類異常

2.1.1文件格式驗證

所謂文件格式驗證就是驗證生成的字節流是否符合clss文件格式的要求,並且能被當前版本的虛擬機處理

  • 是否以魔數0xCAFEBABE開頭

  • 主次版本號是否在當前虛擬機處理範圍之內

  • 檢查常量tag標誌判斷常量池的常量是否有不被支持的常量類型

  • 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量

之所以要有驗證文件格式這一步,是了保證字節流能被正確的解析並存儲在元空間中,之後的元數據驗證,字節碼驗證都是在instanceKlass上進行,不會直接操作字節流

2.1.2元數據驗證

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

  • 檢查這個類是否有父類(所有類的父類都是java.lang.Object)

  • 檢查這個類繼承的父類是否是被final修飾的,final修飾的類不可繼承

  • 如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法

2.1.3字節碼驗證

第二階段對元數據信息中的數據類型做完檢驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件

  • 保證任意時刻操作數棧的數據類型和指令代碼序列都能配合工作,例如不會出現在操作數棧放置了一個int類型的數據,使用時卻按long類型來加載本地變量表
  • 作用域檢驗:保證跳轉指令不會跳轉到方法體以外的字節碼指令上
  • 類型轉換校驗:保證方法體中的類型轉換是有效的,例如可以把一個子類對象賦值給父類數據類型,這是安全的,但是反過來,就是不合法的

如果一個類方法體的字節碼沒有通過字節碼驗證,那肯定有問題,但如果對一個方法體通過了字節碼驗證,也不能說明其一定安全

2.1.4符號引用驗證

  • 符號引用中通過字符串描述的全限定名是否能找到對應的類

  • 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段

符號引用驗證是確保解析能正確進行

2.2準備

準備階段是爲靜態變量分配內存並設置靜態變量初始值的階段,這些變量使用的內存將在方法區分配,注意,實例變量將在對象實例化時隨對象一起分配在java堆中

例如:

    //準備期賦值爲零值
    private static int c;

    //賦值在初始化階段完成
    private static int b = 123;

那麼在準備階段,b在方法區分配內存後,設置的初始值是0而不是123

而賦值爲123是在初始化階段纔會執行

聲明:

在這裏插入圖片描述

這是一個靜態代碼塊,我們沒寫,顯然是自動生成的,在類初始化時執行 ,可以看到,123被壓到操作數棧棧頂,然後在putstatic設置靜態變量b的值爲123,(從常量池中找到的b),可以說明,真正賦值是在初始化時期發生

在這裏插入圖片描述

在這裏插入圖片描述

//static變量是被final修飾的字符串常量值是編譯期可知的,賦值則在準備階段完成private static final String  a = "value123";//準備期賦值爲零值//static變量是被final修飾的基本類型是編譯期可知的,賦值則在準備階段完成private static final int d = 1234;//賦值在初始化階段完成

在看一個例子:

    private static final String a = "aaaa";

    private static String b = "bbbb";

對於b和上邊一樣,都是在初始化時期賦值:

在這裏插入圖片描述

但是對於a呢,被final修飾的靜態變量,其賦值卻是在準備期就已經完成,原因是該值在編譯期可以確定

在這裏插入圖片描述

對於基本類型也一樣

 private static final int a = 123;

在這裏插入圖片描述

所以:

  • 如果是final修飾的靜態變量(字符串類型或者基本類型)在編譯期值可以確定,賦值會在準備期完成
  • 對於如果是final修飾的靜態變量(除string的引用類型)賦值在初始化時期完成

2.3解析

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

什麼是符號引用?

對於上邊我們提到的:

    private static final int a = 123;

    private static String b = "bbbb";

在常量池中是這樣的:

在這裏插入圖片描述

其中a,b便是符號引用

直接引用就是該字段或方法的直接內存地址

解析發生時機

虛擬機規範並沒有規定解析發生的具體時間,只要求在執行anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield,putstatic這16個指令之前,先對他們所引用的符號引用進行解析

三,初始化階段

在準備期,變量已經賦值過一次零值,而在初始化階段是根據程序員的要求去初始化變量值爲指定的值,初始化階段就是執行類構造器 < clinit>()方法的過程

3.1關於< clinit>()方法

  • 該方法是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變量,定義在之後的變量,在前面的靜態語句塊可以賦值,但不能訪問

        static{
            i = 0;
            System.out.println(i);//編譯器會報“非法向前引用”
        }
        static int i = 1;
    
  • 執行順序問題

        static class Parent{
            public static String staticField = "父類靜態變量";
            public String field = "父類普通變量";
            public Parent(){
                System.out.println("父類構造函數");
            }
            static {
                System.out.println(staticField);
                System.out.println("父類靜態代碼塊");
            }
            {
                System.out.println(field);
                System.out.println("父類代碼塊");
            }
        }
    
        static class Sub extends Parent{
            public static String staticField = "子類靜態變量";
            public String field = "子類普通變量";
            public Sub(){
                System.out.println("子類構造函數");
            }
            static {
                System.out.println(staticField);
                System.out.println("子類靜態代碼塊");
            }
            {
                System.out.println(field);
                System.out.println("子類代碼塊");
            }
        }
    
        public static void main(String[] args) {
            new Sub();
        }
    

    該方法執行順序爲:

    在這裏插入圖片描述

  • 該方法也不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不爲這個類生成該方法

  • 虛擬機會確保一個類的這個方法能被正確的加鎖,解鎖

3.2什麼時候對類進行初始化

概括來說,類的初始化是懶惰的

所有的java虛擬機實現必須在每個類或者接口被java程序首次主動使用時才初始化他們

java程序對類的使用方式分爲兩種:

  • 主動使用
    • 創建類實例(new)
    • 訪問某個類或者接口的靜態變量,或者對該靜態變量賦值(putstatic,getstatic)
    • 調用類的靜態方法(invokestatic)
    • 反射
    • 初始化一個類的子類
    • main方法所在類
    • jdk7開始提供的動態語言支持
  • 被動使用

以下是導致類初始化的一些情況:(對應上邊的主動引用)

  • main方法所在的類,總會被首先初始化
  • 首次訪問這個類的靜態變量或者靜態方法時會導致這個類初始化
  • 子類訪問父類的靜態變量,只會觸發父類的初始化
  • 子類初始化時,如果父類還沒初始化,會引發父類的初始化
  • Class.forName等反射包對類進行反射調用時,如果類還沒初始化,會首先觸發其初始化
  • new會導致初始化

下面通過幾個例子去說明

子類訪問父類的靜態變量,只會觸發父類的初始化

    static class Parent{
        public static String staticField = "父類靜態變量";
        public Parent(){
            System.out.println("父類構造函數");
        }
        static {
            System.out.println("父類靜態代碼塊");
        }
    }

    static class Sub extends Parent{
        public static String Field = "子類靜態變量";
        public Sub(){
            System.out.println("子類構造函數");
        }
        static {
            System.out.println("子類靜態代碼塊");
        }
    }

    public static void main(String[] args) {
        System.out.println(Parent.staticField);
    }

結果:(構造函數內容需要執行new纔會調用構造函數,但是執行了靜態代碼塊,證明已經執行了< clinit >方法)

在這裏插入圖片描述

上邊的調用改爲調用子類的靜態變量

這說明:訪問子類的靜態變量會導致子類的初始化,導致子類初始化時如果父類還沒初始化,則父類先初始化

針對上邊兩個例子,說明對於靜態變量,只有直接定義了該字段的類纔會被初始化

在看一個例子

如果是調用final修飾的靜態變量呢

    static class Parent{
        
        public static final String finalField = "靜態final變量";
        public static String staticField = "父類靜態變量";
        public Parent(){
            System.out.println("父類構造函數");
        }
        static {
            System.out.println("父類靜態代碼塊");
        }
    }

    public static void main(String[] args) {
        System.out.println(Parent.finalField);
    }

在這裏插入圖片描述

對於final修飾的靜態變量(字符串類型和基本類型)如果編譯期值可知,那麼就直接在準備期賦值,無需在初始化調用< clinit >方法賦值,上面也說過了

注意,值一定是編譯期可確定的

對於final修飾的值編譯期不確定的,還是會導致直接定義該變量的類初始化

  static class Parent{
        public static final int finalRandomField = new Random().nextInt(2);
        public static final String finalField = "靜態final變量";
        public static String staticField = "父類靜態變量";
        public Parent(){
            System.out.println("父類構造函數");
        }
        static {
            System.out.println("父類靜態代碼塊");
        }
    }

    public static void main(String[] args) {
        System.out.println(Parent.finalRandomField);
    }

對於數組呢

    public static void main(String[] args) {
        //數組類型的初始化是由jvm運行期動態生成
        Parent[] p = new Parent[2];
        //初始化的類是class [LMain$Parent;
        System.out.println(p.getClass());
    }

在這裏插入圖片描述

數組類型的初始化是由jvm運行期動態生成,,不會導致parent初始化

對於靜態方法調用

    static class Parent{
        public static final int finalRandomField = new Random().nextInt(2);
        public static final String finalField = "靜態final變量";
        public static String staticField = "父類靜態變量";
        public Parent(){
            System.out.println("父類構造函數");
        }
        static {
            System.out.println("父類靜態代碼塊");
        }
        public static void staticMethod(){
            System.out.println("調用類的靜態方法");
        }
    }

    public static void main(String[] args) throws ClassNotFoundException {
        Parent.staticMethod();
    }

調用類的靜態方法會導致定義該方法的類初始化,如果有父類,父類先初始化

在這裏插入圖片描述

四,類加載器

4.1類加載器

Bootstrap ClassLoader(啓動類加載器)

  • 這個類加載器負責將一些核心的,被JVM識別的類加載進來,用C++實現,與JVM是一體的。

Extension ClassLoader(擴展類加載器)

  • 這個類加載器用來加載 Java 的擴展庫

Applicaiton ClassLoader(應用程序類加載器)

  • 用於加載我們自己定義編寫的類

User ClassLoader (用戶自己實現的加載器)

  • 當實際需要自己掌控類加載過程時纔會用到,一般沒有用到。

jvm規範允許類加載器在預料到某個類將要被使用時就預先加載他,如果在預先加載過程中遇到.class缺失或者存在錯誤,則類加載器會在程序首次主動使用該類時才報告錯誤,如果這個類一直沒有被程序主動使用,那類加載器也不會報告這個錯誤

4.2雙親委派模型

classloader.jpg

雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,他首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一層次的類加載器都是如此,因此所有的加載請求都最終會傳送到頂層的啓動類加載器,只有當父類加載器反饋自己無法完成這個加載請求時(在他的搜索範圍沒有找到所需的類)子加載器纔會嘗試自己去加載。

爲什麼要這樣?

因爲同一個class文件被不同的類加載器加載後,就會不同的類,所以雙親委派模型保證了一個類只能有一個類加載器去加載

五,類文件結構

在上文中,我們有拿到類似這樣的字節碼

public class Test {

    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public static void main(String[] args) {

    }
}

在這裏插入圖片描述

從上面中,大概可以分爲幾類:

  • 類的描述信息

    在這裏插入圖片描述

  • 類文件結構信息:

    • 版本信息

      在這裏插入圖片描述

    • 類訪問修飾符

      在這裏插入圖片描述

    • 常量池信息

      在這裏插入圖片描述

    • 方法信息

      在這裏插入圖片描述

5.1.class類文件結構

任何一個Class文件都對應着唯一一個類或者接口的定義信息,Class文件是一組以8位字節爲基礎單位的二進制流

在這裏插入圖片描述

主要由一下幾部分組成:

  • 魔數和版本號信息
  • 常量池
  • 類或接口訪問標誌
  • 類索引,父類索引與接口索引集合
  • 字段表
  • 方法表
  • 屬性表

5.1.1魔數和版本號信息

什麼是魔數?

每個class文件的頭4個字節稱爲魔數,他的唯一作用是確定這個文件是否是一個能被虛擬機接受的class文件,class文件的魔數值爲0xCAFEBABE

在這裏插入圖片描述

魔數後邊的4個字節分別是次版本號和主版本號,用於標識class文件的版本信息

高版本的jdk能向下兼容以前版本的class文件,低版本不能向上兼容,虛擬機將會拒絕執行此class文件

5.1.2常量池

常量池是class文件結構中和其他項目關聯最多的數據類型,也是佔用class文件空間最大的數據項目之一,同時還是class文件中第一個出現的表類型數據項目

我們可以將常量池看做是class文件的資源倉庫,java類中定義的方法和變量信息都存儲在常量池中,類中定義的信息由常量池來維護和存儲

public class Test {

    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public static void main(String[] args) {

    }
}

先看這一張圖(上邊Test類的常量池信息):(最左一列是常量池索引值,可以通過索引,定位到常量池的信息)

在這裏插入圖片描述

從上圖可以看出:

  • 常量池是從索引值1開始的,第0項用於存儲常量池的容量計數值
  • 常量池第一項是一個方法引用,從索引爲4找到該方法的類信息
  • 從索引22找到了該類名是java/lang/Object
  • 繼續回到索引19,從索引19找到方法名信息:是Object類的構造方法

下邊看自定義的字段a的常量池信息(探尋的原理也是跟上邊相同)

在這裏插入圖片描述

可以看出,常量池主要存放兩大類常量:

  • 字面量
    • 文本字符串
    • final修飾的編譯期可知的常量值
  • 符號引用
    • 類和接口的全限定名(java/lang/Object)
    • 字段的名稱和描述符 (a)
    • 方法的名稱和描述符([Ljava/lang/String;])V

所以,當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析到具體的內存地址之中

常量池中每一項常量都是一個表,有14種結構,共同特點是表開始的第一位都是一個u1類型的標誌位,代表屬於什麼類型的常量

在這裏插入圖片描述

具體14中項目類型可以看如下的表:(下邊的類型一項中的信息的中間項就對應上邊截圖)

img

5.1.3訪問標誌

在常量池之後緊接着的兩個字節是訪問標誌,用於標識一些類或者接口層次的訪問信息

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YfTG3ErQ-1574957406715)(C:\Users\12642\AppData\Roaming\Typora\typora-user-images\image-20191125180155059.png)]

在16進制中,如使用到則標記位1

这里写图片描述

5.1.4類索引,父類索引和接口索引集合

類索引,父類索引和接口索引集合都按順序排列在訪問標誌之後,類索引和父類索引各自指向一個類型是Class的類描述符常量,通過這個常量中的索引值可以找到該類的全限定類名

在這裏插入圖片描述

接口索引集合,第一項是接口計數器,標識實現接口的數量,如果沒有實現任何接口,則計數器爲0,後邊不佔用任何字節,對於接口也是通過Class類型去找

public class Test implements Serializable {

    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
}


在這裏插入圖片描述

Test繼承關係就體現在了類索引,父類索引和接口索引集合中:

在這裏插入圖片描述

5.1.5字段表集合

字段表用來描述接口或類中聲明的變量信息,包括類變量和實例變量,但不包括方法的局部變量

public class Test implements Serializable {

    private int a;
    private static int b;

    public void setA(int aaaa) {
        this.a = aaaa;
    }
}

字段表包含信息:

  • 訪問修飾符

    这里写图片描述

  • 字段類型

    这里写图片描述

    對於數組類型,每一個維度將使用一個前置的“[”字符來描述。比如定義一個java.lang.String[][]類型的二維數組,將記錄爲[[Ljava/lang/String,一個double數組double[]將標記爲[D。

    當描述符用來描述方法時,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號()內。比如方法void inc()的描述符是:()V。方法java.lang.String toString()的描述符是:()Ljava/lang/String。方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符是:([CII[CIII)I。

5.1.6方法表集合

方法表和字段表結構相似,用來描述類中定義的方法

public class Test implements Serializable {
    
    public String print(){
        int value = 0;
        value++;
        return String.valueOf(value);
    }
}

上邊定義的兩個方法的模型:

在這裏插入圖片描述

以print方法爲例子,可見方法表有:

  • 訪問標誌
  • 名稱索引
  • 描述符索引
  • 屬性表集合

在這裏插入圖片描述

5.1.7屬性表

屬性表在前面出現了多次,在Class文件、字段表和方法表都可以攜帶自己的屬性表集合,來描述某些場景專有的信息。
與Class文件中其他的數據項目要求嚴格的順序、長度和內容不同,屬性表集合的限制比較少,不要求嚴格的順序,只要不與已有的屬性名重複

这里写图片描述

这里写图片描述

六,字節碼指令

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章