《Java虛擬機器原理圖解》1.5、 class檔案中的方法表集合--method方法在class檔案中是怎樣組織的
0. 前言
瞭解JVM虛擬機器原理是每一個Java程式設計師修煉的必經之路。但是由於JVM虛擬機器中有很多的東西講述的比較寬泛,在當前接觸到的關於JVM虛擬機器原理的教程或者部落格中,絕大部分都是充斥的文字性的描述,很難給人以形象化的認知,看完之後感覺還是稀裡糊塗的。
感於以上的種種,我打算把我在學習JVM虛擬機器的過程中學到的東西,結合自己的理解,總結成《Java虛擬機器原理圖解》 這個系列,以圖解的形式,將抽象的JVM虛擬機器的知識具體化,希望能夠對想了解Java虛擬機器原理的的Java程式設計師 提供點幫助。
1.概述
方法表集合是指由若干個方法表(method_info)組成的集合。對於在類中定義的若干個,經過JVM編譯成class檔案後,會將相應的method方法資訊組織到一個叫做方法表集合的結構中,欄位表集合是一個類陣列結構,如下圖所示:
2. method方法的描述-方法表集合在class檔案中的位置
method方法的描述-方法表集合緊跟在欄位表集合的後面(想了解欄位表集合的讀者可以點選我檢視),如下圖所示:
接下來讓我們看看Method_info 結構體是怎麼組織method方法資訊的:
3. 一個類中的method方法應該包含哪些資訊?----method_info結構體的定義
對於一個方法的表示,我們根據我們可以概括的資訊如下所示:
實際上JVM還會對method方法的描述新增其他資訊,我們將在後面詳細討論。如上圖中的method_info結構體的定義,該結構體的定義跟描述field欄位
的field_info
方法表的結構體由:訪問標誌(access_flags)、名稱索引(name_index)、描述索引(descriptor_index)、屬性表(attribute_info)集合組成。
訪問標誌(access_flags):
method_info結構體最前面的兩個位元組表示的訪問標誌(access_flags),記錄這這個方法的作用域、靜態or非靜態、可變性、是否可同步、是否本地方法、是否抽象等資訊,實際上不止這些資訊,我們後面會詳細介紹訪問標誌這兩個位元組的每一位具體表示什麼意思。
名稱索引(name_index):
緊跟在訪問標誌(access_flags)後面的兩個位元組稱為名稱索引,這兩個位元組中的值指向了常量池中的某一個常量池項,這個方法的名稱以UTF-8格式的字串儲存在這個常量池項中。如public void methodName(),很顯然,“methodName”則表示著這個方法的名稱,那麼在常量池中會有一個CONSTANT_Utf8_info格式的常量池項,裡面儲存著“methodName”字串,而mehodName()方法的方法表中的名稱索引則指向了這個常量池項。
描述索引(descriptor_index):
描述索引表示的是這個方法的特徵或者說是簽名,一個方法會有若干個引數和返回值,而若干個引數的資料型別和返回值的資料型別構成了這個方法的描述,其基本格式為: (引數資料型別描述列表)返回值資料型別 。我們將在後面繼續討論。
屬性表(attribute_info)集合:
這個屬性表集合非常重要,方法的實現被JVM編譯成JVM的機器碼指令,機器碼指令就存放在一個Code型別的屬性表中;如果方法宣告要丟擲異常,那麼異常資訊會在一個Exceptions型別的屬性表中予以展現。Code型別的屬性表可以說是非常複雜的內容,也是本文最難的地方。
接下來,我們將一一擊破它們,看看它們到底是怎麼表示的。
4. 訪問標誌(access_flags)---記錄著method方法的訪問資訊
訪問標誌(access_flags)共佔有2 個位元組,分為 16 位,這 16位 表示的含義如下所示:
舉例:某個類中定義瞭如下方法:
greeting()方法的修飾符有:public、static、synchronized、final 這幾個修飾符修飾,那麼相對應地,greeting()方法的訪問標誌中的ACC_PUBLIC、ACC_STATIC、ACC_SYNCHRONIZED、ACC_FINAL標誌位都應該是1,即:public static synchronized final void greeting(){ }
從上圖中可以看出訪問標誌的值應該是二進位制00000000 00111001,即十六進位制0x0039。我們將在文章的最後一個例子中證實這裡點。
5. 名稱索引和描述符索引----一個方法的簽名
緊接著訪問標誌(access_flags)後面的兩個位元組,叫做名稱索引(name_index),這兩個位元組中的值是指向了常量池中某個常量池項的索引,該常量池項表示這這個方法名稱的字串。
方法描述符索引(descrptor_index)是緊跟在名稱索引後面的兩個位元組,這兩個位元組中的值跟名稱索引中的值性質一樣,都是指向了常量池中的某個常量池項。這兩個位元組中的指向的常量池項,是表示了方法描述符的字串。
所謂的方法描述符,實質上就是指用一個什麼樣的字串來描述一個方法,方法描述符的組成如下圖所示:
舉例:對於如下定義的的greeting()方法,我們來看一下對應的method_info結構體中的名稱索引和描述符索引資訊是怎樣組織的。
public static synchronized final void greeting(){
}
如下圖所示,method_info結構體的名稱索引中儲存了一個索引值x,指向了常量池中的第x項,第 x項表示的是字串"greeting",即表示該方法名稱是"greeting";描述符索引中的y
值指向了常量池的第y項,該項表示字串"()V",即表示該方法沒有引數,返回值是void型別。
6.屬性表集合--記錄方法的機器指令和丟擲異常等資訊
屬性表集合記錄了某個方法的一些屬性資訊,這些資訊包括:
- 這個方法的程式碼實現,即方法的可執行的機器指令
- 這個方法宣告的要丟擲的異常資訊
- 這個方法是否被@deprecated註解表示
- 這個方法是否是編譯器自動生成的
屬性表(attribute_info)結構體的一般結構如下所示:
6.1 Code型別的屬性表--method方法中的機器指令的資訊
Code型別的屬性表(attribute_info)可以說是class檔案中最為重要的部分,因為它包含的是JVM可以執行的機器碼指令,JVM能夠執行這個類,就是從這個屬性中取出機器碼的。除了要執行的機器碼,它還包含了一些其他資訊,如下所示:
Code屬性表的組成部分:
機器指令----code:
目前的JVM使用一個位元組表示機器操作碼,即對JVM底層而言,它能表示的機器操作碼不多於2的 8 次方,即 256個。class檔案中的機器指令部分是class檔案中最重要的部分,並且非常複雜,本文的重點不止介紹它,我將專門在一片博文中討論它,敬請期待。
異常處理跳轉資訊---exception_table:
如果程式碼中出現了try{}catch{}塊,那麼try{}塊內的機器指令的地址範圍記錄下來,並且記錄對應的catch{}塊中的起始機器指令地址,當執行時在try塊中有異常丟擲的話,JVM會將catch{}塊對應懂得其實機器指令地址傳遞給PC暫存器,從而實現指令跳轉;
Java原始碼行號和機器指令的對應關係---LineNumberTable屬性表:
編譯器在將java原始碼編譯成class檔案時,會將原始碼中的語句行號跟編譯好的機器指令關聯起來,這樣的class檔案載入到記憶體中並執行時,如果丟擲異常,JVM可以根據這個對應關係,丟擲異常資訊,告訴我們我們的原始碼的多少行有問題,方便我們定位問題。這個資訊不是執行時必不可少的資訊,但是預設情況下,編譯器會生成這一項資訊,如果你項取消這一資訊,你可以使用-g:none 或-g:lines來取消或者要求設定這一項資訊。如果使用了-g:none來生成class檔案,class檔案中將不會有LineNumberTable屬性表,造成的影響就是 將來如果程式碼報錯,將無法定位錯誤資訊報錯的行,並且如果項除錯程式碼,將不能在此類中打斷點(因為沒有指定行號。)
區域性變量表描述資訊----LocalVariableTable屬性表:
區域性變量表資訊會記錄棧幀區域性變量表中的變數和java原始碼中定義的變數之間的關係,這個資訊不是執行時必須的屬性,預設情況下不會生成到class檔案中。你可以根據javac指令的-g:none或者-g:vars選項來取消或者設定這一項資訊。
它有什麼作用呢? 當我們使用IDE進行開發時,最喜歡的莫過於它們的程式碼提示功能了。如果在專案中引用到了第三方的jar包,而第三方的包中的class檔案中有無LocalVariableTable屬性表的區別如下所示:
舉例:
如下定義Simple類,使用javac -g:none Simple.java 編譯出Simple.class 檔案,並使用javap -v Simple > Simple.txt 檢視反編譯的資訊,然後看Simple.class檔案中的方法表集合是怎樣組織的:
package com.louis.jvm; public class Simple { public static synchronized final void greeting(){ int a = 10; } }
1. Simple.class檔案組織資訊如下所示:
如上所示,方法表集合使用了藍色線段圈了起來。
請注意:方法表集合的頭兩個位元組,即方法表計數器(method_count)的值是0x0002,它表示該類中有2 個方法。細心的讀者會注意到,我們的Simple.java中就定義了一個greeting()方法,為什麼class檔案中會顯示有兩個方法呢??
在Simple.classz中出現了兩個方法表,分別代表構造方法<init>()和 greeting()方法,現在讓我們分別來討論這兩個方法:
2. Simple.class 中的<init>() 方法:
解釋:
1. 方法訪問標誌(access_flags): 佔有 2個位元組,值為0x0001,即標誌位的第 16 位為 1,所以該<init>()方法的修飾符是:ACC_PUBLIC;
2. 名稱索引(name_index): 佔有 2 個位元組,值為 0x0004,指向常量池的第 4項,該項表示字串“<init>”,即該方法的名稱是“<init>”;
3.描述符索引(descriptor_index): 佔有 2 個位元組,值為0x0005,指向常量池的第 5 項,該項表示字串“()V”,即表示該方法不帶引數,並且無返回值(建構函式確實也沒有返回值);
4. 屬性計數器(attribute_count): 佔有 2 個位元組,值為0x0001,表示該方法表中含有一個屬性表,後面會緊跟著一個屬性表;
5. 屬性表的名稱索引(attribute_name_index):佔有 2 個位元組,值為0x0006,指向常量池中的第6 項,該項表示字串“Code”,表示這個屬性表是Code型別的屬性表;
6. 屬性長度(attribute_length):佔有4個位元組,值為0x0000 0011,即十進位制的 17,表明後續的 17 個位元組可以表示這個Code屬性表的屬性資訊;
7. 運算元棧的最大深度(max_stack):佔有2個位元組,值為0x0001,表示棧幀中運算元棧的最大深度是1;
8. 區域性變量表的最大容量(max_variable):佔有2個位元組,值為0x0001, JVM在呼叫該方法時,根據這個值設定棧幀中的區域性變量表的大小;
9. 機器指令數目(code_length):佔有4個位元組,值為0x0000 0005,表示後續的5 個位元組 0x2A 、0xB7、 0x00、0x01、0xB1表示機器指令;
10. 機器指令集(code[code_length]):這裡共有 5個位元組,值為0x2A 、0xB7、 0x00、0x01、0xB1;
11. 顯式異常表集合(exception_table_count): 佔有2 個位元組,值為0x0000,表示方法中沒有需要處理的異常資訊;
12. Code屬性表的屬性表集合(attribute_count): 佔有2 個位元組,值為0x0000,表示它沒有其他的屬性表集合,因為我們使用了-g:none 禁止編譯器生成Code屬性表的 LineNumberTable 和LocalVariableTable;
B. Simple.class 中的greeting() 方法:
解釋:
1. 方法訪問標誌(access_flags): 佔有 2個位元組,值為 0x0039 ,即二進位制的00000000 00111001,即標誌位的第11、12、13、16位為1,根據上面講的方法標誌位的表示,可以得到該greeting()方法的修飾符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;
2. 名稱索引(name_index): 佔有 2 個位元組,值為 0x0007,指向常量池的第 7 項,該項表示字串“greeting”,即該方法的名稱是“greeting”;
3. 描述符索引(descriptor_index): 佔有 2 個位元組,值為0x0005,指向常量池的第 5 項,該項表示字串“()V”,即表示該方法不帶引數,並且無返回值;
4. 屬性計數器(attribute_count): 佔有 2 個位元組,值為0x0001,表示該方法表中含有一個屬性表,後面會緊跟著一個屬性表;
5.屬性表的名稱索引(attribute_name_index):佔有 2 個位元組,值為0x0006,指向常量池中的第6 項,該項表示字串“Code”,表示這個屬性表是Code型別的屬性表;
6. 屬性長度(attribute_length):佔有4個位元組,值為0x0000 0010,即十進位制的16,表明後續的16個位元組可以表示這個Code屬性表的屬性資訊;
7. 運算元棧的最大深度(max_stack):佔有2個位元組,值為0x0001,表示棧幀中運算元棧的最大深度是1;
8. 區域性變量表的最大容量(max_variable):佔有2個位元組,值為0x0001, JVM在呼叫該方法時,根據這個值設定棧幀中的區域性變量表的大小;
9. 機器指令數目(code_length):佔有4 個位元組,值為0x0000 0004,表示後續的4個位元組0x10、 0x0A、 0x3B、0xB1的是表示機器指令;
10.機器指令集(code[code_length]):這裡共有4 個位元組,值為0x10、 0x0A、 0x3B、0xB1 ;
11. 顯式異常表集合(exception_table_count): 佔有2 個位元組,值為0x0000,表示方法中沒有需要處理的異常資訊;
12. Code屬性表的屬性表集合(attribute_count): 佔有2 個位元組,值為0x0000,表示它沒有其他的屬性表集合,因為我們使用了-g:none 禁止編譯器生成Code屬性表的 LineNumberTable 和LocalVariableTable;
6.2 Exceptions型別的屬性表----method方法宣告的要丟擲的異常資訊
有些方法在定義的時候,會宣告該方法會丟擲什麼型別的異常,如下定義一個Interface介面,它聲明瞭sayHello()方法,丟擲Exception異常:
package com.louis.jvm; public interface Interface { public void sayHello() throws Exception; }
現在讓我們看一下Exceptions型別的屬性表(attribute_info)結構體是怎樣組織的:
如上圖所示,Exceptions型別的屬性表(attribute_info)結構體由一下元素組成:
屬性名稱索引(attribute_name_index):佔有 2個位元組,其中的值指向了常量池中的表示"Exceptions"字串的常量池項;
屬性長度(attribute_length):它比較特殊,佔有4個位元組,它的值表示跟在其後面多少個位元組表示異常資訊;
異常數量(number_of_exceptions):佔有2 個位元組,它的值表示方法宣告丟擲了多少個異常,即表示跟在其後有多少個異常名稱索引;
異常名稱索引(exceptions_index_table):佔有2個位元組,它的值指向了常量池中的某一項,該項是一個CONSTANT_Class_info型別的項,表示這個異常的完全限定名稱;
舉例:
將上面定義的Interface介面類編譯成class檔案,然後我們檢視Interface.class檔案,找出方法表集合所在位置和相應的資料,並輔助javap -v Inerface 檢視常量池資訊,如下圖所示:
由於sayHello()方法是在的Interface介面類中宣告的,它沒有被實現,所以它對應的方法表(method_info)結構體中的屬性表集合中沒有Code型別的屬性表。
注:
1. 方法計數器(methods_count)中的值為0x0001,表明其後的方法表(method_info)就一個,即我們就定義了一個方法,其後會緊跟著一個方法表(method_info)結構體;
2. 方法的訪問標誌(access_flags)的值是0x0401,二進位制是00000100 00000001,第6位和第16位是1,對應上面的標誌位資訊,可以得出它的訪問標誌符有:ACC_ABSTRACT、ACC_PUBLIC。細心的讀者可能會發現,在上面宣告的sayHello()方法中並沒有宣告為abstract型別啊。確實如此,這是因為編譯器對於介面內宣告的方法自動加上ACC_ABSTRACT標誌。
3. 名稱索引(name_index)中的值為0x0005,0x0005指向了常量池的第5項,第五項表示的字串為“sayHello”,即表示的方法名稱是sayHello
4. 描述符索引(descriptor_index)中的值為0x0006,0x0006指向了常量池中的第6項,第6項表示的字串為“()V” 表示這個方法的無入參,返回值為void型別
5. 屬性表計數器(attribute_count)中的值為0x0001,表示後面的屬性表的個數就1個,後面緊跟著一個attribute_info結構體;
6. 屬性表(attribute_info)中的屬性名稱索引(attribute_name_index)中的值為0x0007,0x0007指向了常量池中的第7 項,第 7項指向字串“Exceptions”,即表示該屬性表表示的異常資訊;
7. 屬性長度(attribute_length)中的值為:0x00000004,即後續的4個位元組將會被解析成屬性值;
8. 異常數量(number_of_exceptions)中的值為0x0001,表示這個方法宣告丟擲的異常個數是1個;
9.異常名稱索引(exception_index_table)中的值為0x0008,指向了常量池中的第8項,第8項表示的是CONSTANT_Class_info型別的常量池項,表示“java/lang/Exception”,即表示此方法丟擲了java.lang.Exception異常。
7. IDE程式碼提示功能實現的基本原理
現在對於企業級的開發,開發者們越來越依賴IDE如Intellij IDEA、Eclipse、MyEclipse、NetBeans等