1. 程式人生 > 實用技巧 >JVM類載入

JVM類載入

>>> hot3.png

執行時資料區

java虛擬機器定義了若干種程式執行時使用到的執行時資料區

1.有一些是 隨虛擬機器的啟動而建立,隨虛擬機器的退出而銷燬

2.第二種則是與執行緒一一對應,隨執行緒的開始和結束而建立和銷燬。

java虛擬機器所管理的記憶體將會包括以下幾個執行時資料區域

PC暫存器

也叫程式計數器(Program Counter Register)是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的訊號指示器。

每一條JVM執行緒都有自己的PC暫存器

在任意時刻,一條JVM執行緒只會執行一個方法的程式碼。該方法稱為該執行緒的當前方法(Current Method)

如果該方法是java方法,那PC暫存器儲存JVM正在執行的位元組碼指令的地址

如果該方法是native,那PC暫存器的值是undefined。

此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

Java虛擬機器棧

與PC暫存器一樣,java虛擬機器棧(Java Virtual Machine Stack)也是執行緒私有的。每一個JVM執行緒都有自己的java虛擬機器棧,這個棧與執行緒同時建立,它的生命週期與執行緒相同。

虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法被呼叫直至執行完成的過程就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

JVM stack 可以被實現成固定大小,也可以根據計算動態擴充套件。

如果採用固定大小的JVM stack設計,那麼每一條執行緒的JVM Stack容量應該線上程建立時獨立地選定。JVM實現應該提供調節JVM Stack初始容量的手段。

如果採用動態擴充套件和收縮的JVM Stack方式,應該提供調節最大、最小容量的手段。

JVM Stack 異常情況:

StackOverflowError:當執行緒請求分配的棧容量超過JVM允許的最大容量時丟擲

OutOfMemoryError:如果JVM Stack可以動態擴充套件,但是在嘗試擴充套件時無法申請到足夠的記憶體去完成擴充套件,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧時丟擲。

Java

在JVM中,堆(heap)是可供各條執行緒共享的執行時記憶體區域,也是供所有類例項和資料物件分配記憶體的區域。

Java堆載虛擬機器啟動的時候就被建立,堆中儲存了各種物件,這些物件被自動管理記憶體系統(Automatic Storage Management System,也即是常說的“Garbage Collector(垃圾回收器)”)所管理。這些物件無需、也無法顯示地被銷燬。

Java堆的容量可以是固定大小,也可以隨著需求動態擴充套件,並在不需要過多空間時自動收縮。

Java堆所使用的記憶體不需要保證是物理連續的,只要邏輯上是連續的即可。

JVM實現應當提供給程式設計師調節Java 堆初始容量的手段,對於可動態擴充套件和收縮的堆來說,則應當提供調節其最大和最小容量的手段。

Java 堆異常:

OutOfMemoryError:如果實際所需的堆超過了自動記憶體管理系統能提供的最大容量時丟擲。

方法區(Method Area

方法區是可供各條執行緒共享的執行時記憶體區域。儲存了每一個類的結構資訊,例如執行時常量池(Runtime Constant Pool)、欄位和方法資料、建構函式和普通方法的位元組碼內容、還包括一些在類、例項、介面初始化時用到的特殊方法

方法區在虛擬機器啟動的時候建立。

方法區的容量可以是固定大小的,也可以隨著程式執行的需求動態擴充套件,並在不需要過多空間時自動收縮。

方法區在實際記憶體空間中可以是不連續的。

Java虛擬機器實現應當提供給程式設計師或者終端使用者調節方法區初始容量的手段,對於可以動態擴充套件和收縮方法區來說,則應當提供調節其最大、最小容量的手段。

Java 方法區異常:

OutOfMemoryError: 如果方法區的記憶體空間不能滿足記憶體分配請求,那Java虛擬機器將丟擲一個OutOfMemoryError異常。

執行時常量池(Runtime Constant Pool

執行時常量池是每一個類或介面的常量池(Constant_Pool)的執行時表現形式,它包括了若干種常量:編譯器可知的數值字面量到必須執行期解析後才能獲得的方法或欄位的引用。

執行時常量池是方法區的一部分。每一個執行時常量池都分配在JVM的方法區中,在類和介面被載入到JVM後,對應的執行時常量池就被建立。

在建立類和介面的執行時常量池時,可能會遇到的異常:

OutOfMemoryError:當建立類和介面時,如果構造執行時常量池所需的記憶體空間超過了方法區所能提供的最大記憶體空間後就會丟擲OutOfMemoryError

本地方法棧

Java虛擬機器可能會使用到傳統的棧來支援native方法(使用Java語言以外的其它語言編寫的方法)的執行,這個棧就是本地方法棧(Native Method Stack)

如果JVM不支援native方法,也不依賴與傳統方法棧的話,可以無需支援本地方法棧。

如果支援本地方法棧,則這個棧一般會線上程建立的時候按執行緒分配。

異常情況:

StackOverflowError:如果執行緒請求分配的棧容量超過本地方法棧允許的最大容量時丟擲

OutOfMemoryError:如果本地方法棧可以動態擴充套件,並且擴充套件的動作已經嘗試過,但是目前無法申請到足夠的記憶體去完成擴充套件,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的本地方法棧,那Java虛擬機器將會丟擲一個OutOfMemoryError異常。

JVM中物件訪問的兩種方式

由於Reference型別在Java虛擬機器規範裡面只規定了一個指向物件的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java堆中的物件的具體位置,因此不同虛擬機器實現的物件訪問方式會有所不同,主流的訪問方式有兩種:使用控制代碼和直接指標:

如果使用控制代碼訪問方式,Java堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料和型別資料各自的具體地址資訊如下圖所示:

如果使用直接指標訪問方式,java堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,reference中直接儲存的就是物件地址。如下圖所示:

這兩種物件的訪問方式各有優勢,使用控制代碼訪問方式的最大好處就是reference中儲存的是穩定的控制代碼地址,在物件被移動時只會改變控制代碼中的例項資料指標,而reference本身不需要被修改。

使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的的時間開銷。

JVM的垃圾回收

注:本文根據《深入理解Java虛擬機器》第3章部分內容整理而成。

一.如何判斷物件是否需要回收?

堆中幾乎放著java世界中的所有的物件例項,垃圾收集器在對堆進行回收前,第一件事就是要確定這些物件哪些還“存活”著,哪些已經“死去”(即不可能再被任何途徑使用的物件)。而如何判斷物件是否應該回收,存在兩個演算法:引用計數演算法(Reference Counting)和根搜尋演算法(GC Roots Tracing) 。但是Java語言中沒有選用引用計數演算法來管理記憶體,主要的原因是它很難解決物件之間的相互迴圈引用的問題。所以下面我們主要介紹根搜尋演算法。

在Java和C#中都是使用根搜尋演算法判定物件是否存活。演算法基本思路就是通過一系列的稱為“GC Roots”的點作為起始進行向下搜尋,當從GC到某一個物件不可達的時候,也就是說一個物件到GC Roots沒有任何引用鏈相連的時候,這個物件就會被判定為可回收的。

在java中,可作為GC Roots的物件包括下面幾種:

1.虛擬機器棧(棧幀中的本地變量表)中的引用的物件。

2.方法區中的類靜態屬性引用的物件。

3.方法區中的常量引用的物件。

4.本地方法棧中JNI(即一般說的Native方法)引用的物件。

根搜尋演算法的圖示如下:

圖:根搜尋演算法判定物件是否可回收

二.垃圾收集演算法

1.標記-清除演算法(Mark-Sweep

這是最基礎的垃圾收集演算法,演算法分為“標記”和“清除”兩 個階段:首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件。它的主要缺點有兩個:一個是效率問題,標記和清除效率都不高;另一個是 空間問題,標記清除後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致分配大物件時沒有足夠的大的連續空間,而不得不提前觸發另一次垃圾收集動作。

圖:標記-清除演算法示意圖

2.複製演算法(Copying)

為了解決效率問題,有了複製的 演算法,他將可用記憶體分為大小相同兩塊。每次只用一塊,當一塊空間用完了,就將還存活的物件複製到另一塊上,然後將剛使用過的記憶體空間一次清理掉。這樣使得 每次都是對其中的一塊進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況。實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小到原來的一半,代價 太貴了點。實際上,新生代中的物件98%都是朝生夕死,所以不需要按11的比例來分記憶體,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中一塊Survivior空間。當回收時,將EdenSurvivor中還存活的物件一次性的拷貝到另一塊Suivivior中,最後清理掉Eden和剛用過的Survivor空間。

圖:複製演算法示意圖

3.標記-整理(Mark-Compact

複製收集演算法在物件存活率高的時候就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保用於應付半區記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。

因此人們提出另外一種“標記-整理”(Mark-Compact)演算法由於老年代中的物件生存週期都較長,有人提出“標記-整理”演算法,標記過程和“標記-清理”一樣,但在清除已死物件的同時會對存活物件進行整理,這樣可以減少碎片空間。

圖:標記-整理演算法示意圖

4.分代收集

當前商業虛擬機器的垃圾收集都是採用分代收集Generational Collecting)演算法,這種演算法並沒有什麼新的思想出現,只是根據物件不同的存活週期將記憶體劃分為幾塊。一般是把Java堆分作新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就用複製演算法,只要少量複製成本就可以完成收集。而老年代中因為物件的存活率較高、週期長,就用標記-整理標記-清除演算法來回收。

Class檔案結構

一、Java的語言無關性

到今天為止,或許大部分的程式設計師都還認為Java虛擬機器執行Java程式是一件理所當然和天經地義的事情。但在Java發展之初,設計者們就考慮過了在 Java虛擬機器上執行其它語言的可能性。時至今日商業機構和開源機構以及在Java語言之外發展出一大批在Java虛擬機器上執行的語言,如 Clojure,Groovy,JRuby,Jython,Scala,等等。

實現語言無關性的基礎仍然是虛擬機器和位元組碼儲存格式,使用Java編譯器可以把Java程式碼編譯為儲存位元組碼的Class檔案,使用JRuby等其它語言 的編譯器一樣可以把程式編譯成Class檔案,虛擬機器不需要關心Class來源於什麼語言,只要它符合Class檔案應用的結構就可以在Java虛擬機器中 執行。如下圖所示:

圖1:Java虛擬機器提供的語言無關性

二、Class類檔案的結構

Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案之中。當遇到需要佔用8個位元組以上空間的資料項時,則會按照高位在前的方式分割成若干個8位位元組進行儲存。

根據Java虛擬機器規範的規定,Class檔案格式採用一種類似於C語言結構體的偽結構來儲存,這種偽結構只有兩種資料型別:無符號數和表。

無符號數屬於基本的資料型別,以u1,u2,u4,u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數。

表是由多個無符號數或其他表作為資料項組成的符合資料型別。表用於描述有層次關係的複合結構的資料,整個Class檔案本質上就是一張表,由下圖所示的資料項構成:

圖2:Class檔案格式

下面首先給出一段簡單的Java程式,以這段程式碼編譯成的Class檔案為基礎進行Class檔案結構的介紹,程式碼如下:

public class TestClass{
    private int num;

    public int inc(){
        for(int i=0; i<10; i++){
            num = i;
        }
        return num;
    }

    public static void main(String[] args){
        new TestClass().inc();
    }
}

把程式碼編譯成Class檔案後,使用16進位制編輯器WinHex開啟這個Class檔案,可以看到Class的內部結構如下:

圖3:Class檔案的結構

2.1 魔數與Class檔案的版本

從上面的圖2可以看出,Class檔案格式表中,第一項為一個u4型別的名為magic的資料項。這是因為每個Class檔案的頭4個位元組稱為魔數 (Magic Number),它的唯一作用就是用來確定這個檔案是否為一個能被虛擬機器接受的Class檔案。很多檔案儲存標準中都使用魔數來進行身份識別。例如圖片格 式。使用魔數而不是副檔名來進行識別主要是基於安全考慮,因為副檔名可以很隨便地被人為改動。

Class檔案的魔數的值為:0xCAFFBABE,這個名稱的由來據說還有個很有趣的典故,在這裡就不詳細表述了,有興趣的可以百度或者google!

class檔案魔數值如下圖所示:

圖4:Class檔案的魔數

緊接著魔數的4個位元組儲存的是Class檔案的版本號:第5和第6位元組是次版本號(Minor Version),第7個和第8個位元組是主版本號(Major Version)。Java的版本號是從45開始的,Jdk1.1之後的每個JDK版本釋出主版本號向上加1,高版本的JDK能向下相容以前版本的 Class檔案,但不能執行以後版本的Class檔案。

根據上面的圖4所示,代表版本號的第5個位元組和第6個位元組值為0x0000,而主版本號的值為0x0032,即十進位制的50,該版本號說明這個是可以被JDK1.6或以上版本的虛擬機器執行的Class檔案。

下圖列出了從jdk1.1到jdk1.7之間,主流的JDK版本編譯器輸出的預設的和可支援的Class檔案版本號:

圖5:Class檔案版本號

2.2 常量池

緊接著主次版本號之後的是常量池入口,常量池是Class檔案結構中與其他專案關聯最多的資料型別。常量池之中主要存放兩大類常量:字面量 (Literal)和符號引用(Symbolic Reference)。字面量比較接近於Java語言層面的常量概念,如文字字串、被宣告為final的常量值等。而符號引用則屬於編譯原理方面的概 念,包括了下面三類常量:

1.類和介面的全限定名(Fully Qualified Name)

2.欄位的名稱和描述符(Descriptor)

3.方法的名稱和描述符。

使用Javap命令輸出常量表

D:\JVM>javap -verbose TestClass
        Compiled from "TestClass.java"
public class TestClass extends java.lang.Object  
  SourceFile: "TestClass.java"
        minor version: 0
        major version: 50
        Constant pool:
        const #1 = Method       #6.#20; //  java/lang/Object."<init>":()V  
        const #2 = Field        #3.#21; //  TestClass.num:I  
        const #3 = class        #22;    //  TestClass  
        const #4 = Method       #3.#20; //  TestClass."<init>":()V  
        const #5 = Method       #3.#23; //  TestClass.inc:()I  
        const #6 = class        #24;    //  java/lang/Object  
        const #7 = Asciz        num;
        const #8 = Asciz        I;
        const #9 = Asciz        <init>;  
const #10 = Asciz       ()V;
        const #11 = Asciz       Code;
        const #12 = Asciz       LineNumberTable;
        const #13 = Asciz       inc;
        const #14 = Asciz       ()I;
        const #15 = Asciz       StackMapTable;
        const #16 = Asciz       main;
        const #17 = Asciz       ([Ljava/lang/String;)V;
        const #18 = Asciz       SourceFile;
        const #19 = Asciz       TestClass.java;
        const #20 = NameAndType #9:#10;//  "<init>":()V  
        const #21 = NameAndType #7:#8;//  num:I  
        const #22 = Asciz       TestClass;
        const #23 = NameAndType #13:#14;//  inc:()I  
        const #24 = Asciz       java/lang/Object;

        {
public TestClass();
        Code:
        Stack=1, Locals=1, Args_size=1
        0:   aload_0
        1:   invokespecial   #1; //Method java/lang/Object."<init>":()V  
        4:   return
        LineNumberTable:
        line 1: 0

public int inc();
        Code:
        Stack=2, Locals=2, Args_size=1
        0:   iconst_0
        1:   istore_1
        2:   iload_1
        3:   bipush  10
        5:   if_icmpge       19
        8:   aload_0
        9:   iload_1
        10:  putfield        #2; //Field num:I  
        13:  iinc    1, 1
        16:  goto    2
        19:  aload_0
        20:  getfield        #2; //Field num:I  
        23:  ireturn
        LineNumberTable:
        line 5: 0
        line 6: 8
        line 5: 13
        line 8: 19

        StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
        offset_delta = 2
        locals = [ int ]
        frame_type = 250 /* chop */
        offset_delta = 16

public static void main(java.lang.String[]);
        Code:
        Stack=2, Locals=1, Args_size=1
        0:   new     #3; //class TestClass  
        3:   dup
        4:   invokespecial   #4; //Method "<init>":()V  
        7:   invokevirtual   #5; //Method inc:()I  
        10:  pop
        11:  return
        LineNumberTable:
        line 12: 0
        line 13: 11

        }

2.3 訪問標誌

在常量池結束之後,緊接著的兩個位元組代表訪問標誌(access_flags),這個標誌用於識別一些類或介面層次的訪問資訊,包括:這個Class是 介面還是類;是否定義為public型別;是否定義為abstract型別;如果是類的話,是否宣告為final,等等。

2.4 類索引、父類索引與介面索引集合

類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引集合(interfaces)是一組u2型別 的資料的集合,Class檔案中由這三項資料來確定這個類的繼承關係。類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。 Java不允許多重繼承,所以父類索引只有一個,除了java.lang.Object外,所有Java類的父類索引都不為0。介面索引集合就用來描述這 個類實現了哪些介面,所有被實現的介面按類定義中的implements(如果類是一個介面則是extends)後的介面順序從左到右排列在介面的索引集 閤中。

2.5 欄位表集合

欄位表(field_info)用於描述介面或類中宣告的變數。欄位(field)包括了類級變數和例項級變數,但不包括方法內部宣告的變數。一個欄位 的資訊包括:作用域(public、private、protected修飾符)、是例項變數還是類變數(static修飾符)、可變性(final)、 併發可見性(volatile修飾符,是否強制從主記憶體讀寫)、可否序列化(transient修飾符)、欄位資料型別(基本資料型別、物件、陣列)、字 段名稱。這些資訊中,各個修飾符都是布林值,要麼有,要麼沒有。

全限定名稱:如果TestClass類是定義在com.chenzhou.jvm包中,那麼這個類的全限定名為com/chenzhou/jvm/TestClass。

簡單名稱:簡單名稱指沒有型別和引數修飾的方法或欄位名稱,在上面的例子中,TestClass類中的inc()方法和num欄位的簡單名稱分別為“inc”和“num”。

描述符:描述符的作用是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序)和返回值。根據描述符規則,基本資料型別 (byte,char,double,float,int,long,short,boolean)及代表無返回值的void型別都用一個大寫字元來表 示,而物件則用字元L加物件的全限定名來表示,如下圖所示:

圖6:描述符標識字元含義

對於陣列型別,每一維度將使用一個前置的“[”字元來描述,如定義一個“java.lang.String[][]”型別的二維陣列,將會被記錄為:"[[Ljava/lang/String;",一個整型陣列"int[]"將被記錄為"[I"。

用描述符來描述方法時,按照先引數列表,後返回值的順序描述,引數列表按照嚴格的順序放在一組小括號"()"內,如方法void inc()的描述符為"()V",方法java.lang.String.toString()的描述符為"()Ljava/lang /String;",方法int indexOf(char[] source,int offset,int count,char[] target,int tOffset,int tCount,int fromIndex)的描述符為"([CII[CIII)I"。

2.6 方法表集合

Class檔案儲存格式中對方法的描述與對欄位的描述幾乎完全一致。方法表的結構如同欄位表一樣,依次包括了訪問標誌、名稱索引、描述符索引、屬性表集合幾項。

2.7 屬性表集合

Java虛擬機器規範第二版中預定義了9項虛擬機器應當能識別的屬性,包括:Code、ConstantValue、Deprecated、 Exceptions、InnerClasses、LineNumberTable、LocalVariableTable、SourceFile、 Synthetic。

根據位元組碼指令介紹方法執行流程

在上一篇部落格中介紹了《Class檔案結構》,其中就提到了一個例子,下面我們依然根據該例子的位元組碼來對方法的執行流程進行講解。

java類原始碼如下:

public class TestClass{
    private int num;

    public int inc(){
        for(int i=0; i<10; i++){
            num = i;
        }
        return num;
    }

    public static void main(String[] args){
        new TestClass().inc();
    }
}

使用javap -verbose命令反編譯後,輸出常量表和位元組碼如下:

D:\JVM>javap -verbose TestClass
        Compiled from "TestClass.java"
public class TestClass extends java.lang.Object  
  SourceFile: "TestClass.java"
        minor version: 0
        major version: 50
        Constant pool:
        const #1 = Method       #6.#20; //  java/lang/Object."<init>":()V  
        const #2 = Field        #3.#21; //  TestClass.num:I  
        const #3 = class        #22;    //  TestClass  
        const #4 = Method       #3.#20; //  TestClass."<init>":()V  
        const #5 = Method       #3.#23; //  TestClass.inc:()I  
        const #6 = class        #24;    //  java/lang/Object  
        const #7 = Asciz        num;
        const #8 = Asciz        I;
        const #9 = Asciz        <init>;  
const #10 = Asciz       ()V;
        const #11 = Asciz       Code;
        const #12 = Asciz       LineNumberTable;
        const #13 = Asciz       inc;
        const #14 = Asciz       ()I;
        const #15 = Asciz       StackMapTable;
        const #16 = Asciz       main;
        const #17 = Asciz       ([Ljava/lang/String;)V;
        const #18 = Asciz       SourceFile;
        const #19 = Asciz       TestClass.java;
        const #20 = NameAndType #9:#10;//  "<init>":()V  
        const #21 = NameAndType #7:#8;//  num:I  
        const #22 = Asciz       TestClass;
        const #23 = NameAndType #13:#14;//  inc:()I  
        const #24 = Asciz       java/lang/Object;

        {
public TestClass();
        Code:
        Stack=1, Locals=1, Args_size=1
        0:   aload_0
        1:   invokespecial   #1; //Method java/lang/Object."<init>":()V  
        4:   return
        LineNumberTable:
        line 1: 0

public int inc();
        Code:
        Stack=2, Locals=2, Args_size=1
        0:   iconst_0        //定義一個常量0,放入運算元棧  
        1:   istore_1        //把該常量彈出棧頂存入到區域性變量表  
        2:   iload_1         //把該區域性變數放入運算元棧  
        3:   bipush  10      //把常量10放入運算元棧  
        5:   if_icmpge   19  //i10進行比較  
        8:   aload_0         //載入區域性變量表index0的變數放入運算元棧  
        9:   iload_1         //載入區域性變量表index1的變數放入運算元棧  
        10:  putfield        #2; //Field num:I   //i的值賦給num欄位  
        13:  iinc    1, 1    //區域性變數i自增1  
        16:  goto    2       //跳轉到第2        19:  aload_0         //載入區域性變量表index0的變數放入運算元棧  
        20:  getfield        #2; //Field num:I   //獲取欄位num的值  
        23:  ireturn         //返回  
        LineNumberTable:
        line 5: 0
        line 6: 8
        line 5: 13
        line 8: 19

        StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
        offset_delta = 2
        locals = [ int ]
        frame_type = 250 /* chop */
        offset_delta = 16

public static void main(java.lang.String[]);
        Code:
        Stack=2, Locals=1, Args_size=1
        0:   new     #3; //class TestClass  
        3:   dup
        4:   invokespecial   #4; //Method "<init>":()V //呼叫例項初始化方法  
        7:   invokevirtual   #5; //Method inc:()I        //呼叫普通方法inc()  
        10:  pop
        11:  return
        LineNumberTable:
        line 12: 0
        line 13: 11

        }

方法的呼叫指令分為以下幾種:

  1. invokevirtual指令用於呼叫所有的虛方法。
  2. invokeinterface指令用於呼叫介面方法,它會在執行時搜尋一個實現了這個介面方法的物件,找出適合的方法進行呼叫。
  3. invokespecial指令用於呼叫一些需要特殊處理的例項方法,包括例項構造器<init>化方法、私有方法和父類方法。
  4. invokestatic指令用於呼叫靜態方法(static方法)。

其它具體的指令可以參考:位元組碼指令集

類載入的時機

本文根據《深入理解java虛擬機器》第7章部分內容整理

Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的載入機制。

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括了:載入(Loading)、驗證(Verification)、準備 (Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和解除安裝 (Unloading)七個階段。其中驗證、準備和解析三個部分統稱為連線(Linking),這七個階段的發生順序如下圖所示:

如上圖所示,載入、驗證、準備、初始化和解除安裝這五個階段的順序是確定的,類的載入過程必須按照這個順序來按部就班地開始,而解析階段則不一定,它在某些情況下可以在初始化階段後再開始。

類的生命週期的每一個階段通常都是互相交叉混合式進行的,通常會在一個階段執行的過程中呼叫或啟用另外一個階段。

Java虛擬機器規範沒有強制性約束在什麼時候開始類載入過程,但是對於初始化階段,虛擬機器規範則嚴格規定了有且只有四種情況必需立即對類進行“初始化”(而載入、驗證、準備階段則必需在此之前開始),這四種情況歸類如下:

1.遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始 化。生成這4條指令最常見的Java程式碼場景是:使用new關鍵字例項化物件時、讀取或者設定一個類的靜態欄位(被final修飾、已在編譯器把結果放入 常量池的靜態欄位除外)時、以及呼叫一個類的靜態方法的時候。

2.使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

3.當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要觸發父類的初始化。

4.當虛擬機器啟動時,使用者需要指定一個執行的主類(包含main()方法的類),虛擬機器會先初始化這個類。

對於這四種觸發類進行初始化的場景,在java虛擬機器規範中限定了“有且只有”這四種場景會觸發。這四種場景的行為稱為對類的主動引用,除此以外的所有引用類的方式都不會觸發類的初始化,稱為被動引用。

下面通過三個例項來說明被動引用:

示例一

父類SuperClass.java

package com.chenzhou.classloading;
/**
 * ClassName:SuperClass <br/> 
 * Function: 被動使用類:通過子類引用父類的靜態欄位,不會導致子類初始化. <br/> 
 * Date:     2012-7-18 上午09:37:06 <br/> 
 * @author   chenzhou
 * @version
 * @since    JDK 1.6 
 * @see
 */
public class SuperClass {
    static{
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}

子類SubClass.java

package com.chenzhou.classloading;

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

主類NotInitialization.java

package com.chenzhou.classloading;
/**
 * ClassName:NotInitialization <br/> 
 * Function: 非主動使用類欄位演示. <br/> 
 * Date:     2012-7-18 上午09:41:14 <br/> 
 * @author   chenzhou
 * @version
 * @since    JDK 1.6 
 * @see
 */
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

輸出結果:

SuperClass init!

123

由結果可以看出只輸出了“SuperClass init!”,沒有輸出“SubClass init!”。這是因為對於靜態欄位,只有直接定義該欄位的類才會被初始化,因此當我們通過子類來引用父類中定義的靜態欄位時,只會觸發父類的初始化,而不會觸發子類的初始化。

示例二

父類SuperClass.java如上一個示例一樣

主類NotInitialization.java

package com.chenzhou.classloading;
/**
 * ClassName:NotInitialization <br/> 
 * Function: 通過陣列定義來引用類,不會觸發此類的初始化. <br/> 
 * Date:     2012-7-18 上午09:41:14 <br/> 
 * @author   chenzhou
 * @version
 * @since    JDK 1.6 
 * @see
 */
public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] scs = new SuperClass[10];
    }
}

輸出結果為空

沒有輸出“SuperClass init!”說明沒有觸發類com.chenzhou.classloading.SuperClass的初始化階段,但是這段程式碼會觸發 “[Lcom.chenzhou.classloading.SuperClass”類的初始化階段。這個類是由虛擬機器自動生成的,該建立動作由 newarray觸發。

示例三

常量類ConstClass.java

package com.chenzhou.classloading;
/**
 * ClassName:ConstClass <br/> 
 * Function: 常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化. <br/> 
 * Reason:   TODO ADD REASON. <br/> 
 * Date:     2012-7-18 上午09:46:56 <br/> 
 * @author   chenzhou
 * @version
 * @since    JDK 1.6 
 * @see
 */
public class ConstClass {
    static{
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

主類NotInitialization.java

package com.chenzhou.classloading;
/**
 * ClassName:NotInitialization <br/> 
 * Function: 非主動實用類欄位演示. <br/> 
 * Date:     2012-7-18 上午09:41:14 <br/> 
 * @author   chenzhou
 * @version
 * @since    JDK 1.6 
 * @see
 */
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

輸出:hello world

上面的示例程式碼執行後也沒有輸出“SuperClass init!”,這是因為雖然在Java原始碼中引用了ConstClass類中的常量HELLOWORLD,但是在編譯階段將此常量的值“hello world”儲存到了NotInitialization類的常量池中,對於常量ConstClass.HELLOWORLD的引用實際上都被轉化為 NotInitialization類對自身常量池的引用了。實際上NotInitialization的Class檔案之中已經不存在 ConstClass類的符號引用入口了。

介面的載入過程與類載入的區別在於上面提到的四種場景中的第三種,當類在初始化時要求其父類都已經初始化過了,但是一個介面在初始化時,並不要求其父類都完成了初始化,只有在真正用到父類介面的時候(如引用父介面的常量)才會初始化。

類載入的過程

在我的上一篇文章《JVM學習筆記(六):類載入的時機》中提到了java類從載入到解除安裝過程包括了加 載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(using)、和解除安裝(Unloading)七個階段。下面我們來詳細講解一下類載入的全過程,也就是載入、驗 證、準備、解析和初始化這五個階段的過程。

一.載入

首先要說明的是“載入”(Loading)階段只是“類載入”(Class Loading)過程的一個階段。不要混淆了這兩個概念。在載入階段,虛擬機器需要完成以下三件事情:

1.通過一個類的許可權定名稱來獲取定義此類的二進位制位元組流。

2.將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

3.在java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。

相對於類載入過程的其他階段,載入階段是開發期相對來說可控性比較強,該階段既可以使用系統提供的類載入器完成,也可以由使用者自定義的類載入器來完成,開發人員可以通過定義自己的類載入器去控制位元組流的獲取方式。

二.驗證

驗證是連線(linking)階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

不同的虛擬機器對類驗證的實現可能會有所不同,但大致上都會完成下面四個階段的檢驗過程:檔案格式驗證、元資料驗證、位元組碼驗證和符號引用驗證。

1.檔案格式驗證:該階段主要是驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。

2.元資料驗證:這一階段主要是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。

3.位元組碼驗證:這一階段是整個驗證階段最複雜的一個階段,主要工作是進行資料流和控制流分析。在第二階段對元資料資訊中的資料型別做完校驗後,這階段將對類的方法體進行校驗分析。這階段的任務是保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為。

4.符號引用驗證:主要是在虛擬機器將符號引用轉化為直接引用的時候進行校驗,這個轉化動作是發生在解析階段。符號引用可以看做是對類自身以外(常量池的各種符號引用)的資訊進行匹配性的校驗。

驗證階段對於虛擬機器的類載入機制來說,是一個非常重要但不一定是必要的階段。如果所執行的全部程式碼都已經被反覆使用和驗證過,在實施階段就可以考慮使用-Xverify:none引數來關閉大部分的類驗證措施,從而縮短虛擬機器類載入的時間。

三.準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。注:這個時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起被分配在Java堆中。

四.解析

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

符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經在記憶體中。

直接引用(Direct Reference):直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一個 符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在記憶體中存在。

對於同一個符號引用可能會出現多次解析,虛擬機器可能會對第一次解析的結果進行快取。

解析動作分為四類:包括類或介面的解析、欄位解析、類方法解析、介面方法解析。

五.初始化

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

初始化階段是執行類構造器<clinit>()方法的過程。對於<clinit>()方法具體介紹如下:

1)<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序由語句在原始檔中出現的順序所決定。

2)<clinit>()方法與類的建構函式不同,它不需要顯式地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方 法執行之前,父類的<clinit>()方法已經執行完畢,因此在虛擬機器中第一個執行的<clinit>()方法的類一定是 java.lang.Object。

3)由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。如下面的例子所示,輸出結果為2而不是1。

public class Parent {
    public static int A = 1;
    static{
        A = 2;
    }
}

public class Sub extends Parent{
    public static int B = A;
}

public class Test {
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}

4)<clinit>()方法對於類或者介面來說並不是必需的,如果一個類中沒有靜態語句塊也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法。

5)介面中可能會有變數賦值操作,因此介面也會生成<clinit>()方法。但是介面與類不同,執行介面的<clinit> ()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數被使用時,父接口才會被初始化。另外,介面的實現類在初始 化時也不會執行介面的<clinit>()方法。

6)虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖和同步。如果有多個執行緒去同時初始化一個類,那麼只會有一個線 程去執行這個類的<clinit>()方法,其它執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢。如果在一 個類的<clinit>()方法中有耗時很長的操作,那麼就可能造成多個程序阻塞。

類載入器以及雙親委派模型

1.什麼是類載入器?

在類載入階段,有一步是“通過類的全限定名來獲取描述此類的二進位制位元組流”,而所謂的類載入器就是實現這個功能的一個程式碼模組,這個動作是在Java虛擬機器外部實現的,這樣做可以讓應用程式自己決定如何去獲取所需要的類。

類載入器的作用:首先類載入器可以實現最本質的功能即類的載入動作。同時,它還能夠結合java類本身來確定該類在Java虛擬機器中的唯一性。用通俗的 話來說就是:比較兩個類是否相等,只有這兩個類是由同一個類載入器載入才有意義。否則,即使這兩個類是來源於同一個Class檔案,只要載入它們的類載入 器不同,那麼這兩個類必定不相等。

2.雙親委派模型

從虛擬機器的角度來說,只存在兩種不同的類載入器:一種是啟動類載入器(Bootstrap ClassLoader),該類載入器使用C++語言實現,屬於虛擬機器自身的一部分。另外一種就是所有其它的類載入器,這些類載入器是由Java語言實 現,獨立於JVM外部,並且全部繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,大部分Java程式一般會使用到以下三種系統提供的類載入器:

1)啟動類載入器(Bootstrap ClassLoader):負責載入JAVA_HOME\lib目錄中並且能被虛擬機器識別的類庫到JVM記憶體中,如果名稱不符合的類庫即使放在lib目錄中也不會被載入。該類載入器無法被Java程式直接引用。

2)擴充套件類載入器(Extension ClassLoader):按《深入理解java虛擬機器》這本書上所說,該載入器主要是負責載入JAVA_HOME\lib\ext目錄中的類庫,但是貌似在JDK的安裝目錄下,沒看到該指定的目錄。該載入器可以被開發者直接使用。

3)應用程式類載入器(Application ClassLoader):該類載入器也稱為系統類載入器,它負責載入使用者類路徑(Classpath)上所指定的類庫,開發者可以直接使用該類載入器, 如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

我們的應用程式都是由這三類載入器互相配合進行載入的,我們也可以加入自己定義的類載入器。這些類載入器之間的關係如下圖所示:

如上圖所示的類載入器之間的這種層次關係,就稱為類載入器的雙親委派模型(Parent Delegation Model)。該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。子類載入器和父類載入器不是以繼承 (Inheritance)的關係來實現,而是通過組合(Composition)關係來複用父載入器的程式碼。

雙親委派模型的工作過程為:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父 類載入器去完成,每一個層次的載入器都是如此,因此所有的類載入請求都會傳給頂層的啟動類載入器,只有當父載入器反饋自己無法完成該載入請求(該載入器的 搜尋範圍中沒有找到對應的類)時,子載入器才會嘗試自己去載入

使用這種模型來組織類載入器之間的關係的好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如java.lang.Object 類,無論哪個類載入器去載入該類,最終都是由啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。否則的話,如果不使用該 模型的話,如果使用者自定義一個java.lang.Object類且存放在classpath中,那麼系統中將會出現多個Object類,應用程式也會變 得很混亂。如果我們自定義一個rt.jar中已有類的同名Java類,會發現JVM可以正常編譯,但該類永遠無法被載入執行。

在rt.jar包中的java.lang.ClassLoader類中,我們可以檢視類載入實現過程的程式碼,具體原始碼如下:

//有兩個loadClass()方法,不會最終都是呼叫第二個方法  
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

//最終呼叫的loadClass()方法   
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    //非同步保護,防止重複載入一個相同的類  
    synchronized (getClassLoadingLock(name)) {
        // 檢測這個類是否被載入了  
        Class c = findLoadedClass(name);
        //如果沒有被載入,首先讓父類去載入  
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//父類載入器不是null,也就是說不是BootStrap載入器  
                    c = parent.loadClass(name, false);
                } else {//父類載入器是BootStrap載入器  
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found  
                // from the non-null parent class loader  
            }
            //如果經過父類載入器之後,還是null,也就是說父類無法載入,那麼再由自己完成載入  
            if (c == null) {
                // If still not found, then invoke findClass in order  
                // to find the class.  
                long t1 = System.nanoTime();
                c = findClass(name);//呼叫自己的findClass()方法  

                // this is the defining class loader; record the stats  
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

通過上面程式碼可以看出,雙親委派模型是通過loadClass()方法來實現的,根據程式碼以及程式碼中的註釋可以很清楚地瞭解整個過程其實非常簡單:先檢 查是否已經被載入過,如果沒有則呼叫父載入器的loadClass()方法,如果父載入器為空則預設使用啟動類載入器作為父載入器。如果父類載入器載入失 敗,則先丟擲ClassNotFoundException,然後再呼叫自己的findClass()方法進行載入。

轉載於:https://my.oschina.net/fuyong/blog/724291