瞭解JVM虛擬機原理是每個Java程序員修煉的必經之路。可是因爲JVM虛擬機中有不少的東西講述的比較寬泛,在當前接觸到的關於JVM虛擬機原理的教程或者博客中,絕大部分都是充斥的文字性的描述,很難給人以形象化的認知,看完以後感受仍是稀裏糊塗的。
html
感於以上的種種,我打算把我在學習JVM虛擬機的過程當中學到的東西,結合本身的理解,總結成《Java虛擬機原理圖解》 這個系列,以圖解的形式,將抽象的JVM虛擬機的知識具體化,但願可以對想了解Java虛擬機原理的的Java程序員 提供點幫助。java
方法表集合是指由若干個方法表(method_info)組成的集合。對於在類中定義的若干個,通過JVM編譯成class文件後,會將相應的method方法信息組織到一個叫作方法表集合的結構中,字段表集合是一個類數組結構,以下圖所示:ui
method方法的描述-方法表集合緊跟在字段表集合的後面(想了解字段表集合的讀者能夠點擊我查看),以下圖所示:編碼
接下來讓咱們看看Method_info 結構體是怎麼組織method方法信息的:lua
對於一個方法的表示,咱們根據咱們能夠歸納的信息以下所示:
實際上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類型的屬性表能夠說是很是複雜的內容,也是本文最難的地方。
接下來,咱們將一一擊破它們,看看它們究竟是怎麼表示的。
訪問標誌(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。咱們將在文章的最後一個例子中證明這裏點。
緊接着訪問標誌(access_flags)後面的兩個字節,叫作名稱索引(name_index),這兩個字節中的值是指向了常量池中某個常量池項的索引,該常量池項表示這這個方法名稱的字符串。
方法描述符索引(descrptor_index)是緊跟在名稱索引後面的兩個字節,這兩個字節中的值跟名稱索引中的值性質同樣,都是指向了常量池中的某個常量池項。這兩個字節中的指向的常量池項,是表示了方法描述符的字符串。
所謂的方法描述符,實質上就是指用一個什麼樣的字符串來描述一個方法,方法描述符的組成以下圖所示:
關於不一樣的數據類型的描述符是怎樣的,我已經在《Java虛擬機原理圖解》1.4 class文件中的字段表集合--field字段在class文件中是怎樣組織的 第五部分字段的數據類型表示和字段名稱表示 進行過詳細的闡釋,感興趣的讀者能夠前去查看。
舉例:對於以下定義的的greeting()方法,咱們來看一下對應的method_info結構體中的名稱索引和描述符索引信息是怎樣組織的。
-
public static synchronized final void greeting(){
-
}
以下圖所示,method_info結構體的名稱索引中存儲了一個索引值x,指向了常量池中的第x項,第 x項表示的是字符串"greeting",即表示該方法名稱是"greeting";描述符索引中的y 值指向了常量池的第y項,該項表示字符串"()V",即表示該方法沒有參數,返回值是void類型。
屬性表集合記錄了某個方法的一些屬性信息,這些信息包括:
- 這個方法的代碼實現,即方法的可執行的機器指令
- 這個方法聲明的要拋出的異常信息
- 這個方法是否被@deprecated註解表示
- 這個方法是不是編譯器自動生成的
屬性表(attribute_info)結構體的通常結構以下所示:
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 、0xB七、 0x00、0x0一、0xB1表示機器指令;
10. 機器指令集(code[code_length]):這裏共有 5個字節,值爲0x2A 、0xB七、 0x00、0x0一、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,即標誌位的第十一、十二、1三、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個字節0x十、 0x0A、 0x3B、0xB1的是表示機器指令;
10.機器指令集(code[code_length]):這裏共有4 個字節,值爲0x十、 0x0A、 0x3B、0xB1 ;
11. 顯式異常表集合(exception_table_count): 佔有2 個字節,值爲0x0000,表示方法中沒有須要處理的異常信息;
12. Code屬性表的屬性表集合(attribute_count): 佔有2 個字節,值爲0x0000,表示它沒有其餘的屬性表集合,由於咱們使用了-g:none 禁止編譯器生成Code屬性表的 LineNumberTable 和LocalVariableTable;
有些方法在定義的時候,會聲明該方法會拋出什麼類型的異常,以下定義一個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異常。
如今對於企業級的開發,開發者們愈來愈依賴IDE如Intellij IDEA、Eclipse、MyEclipse、NetBeans等,利用他們提供的高級功能,能夠極大地提升編碼的速度和效率。
每一個IDE都提供了代碼提示功能,它們實現的基本原理其實就是IDE針對它們項目下的包中全部的class文件進行建模,解析出它們的方法信息,當咱們必定的條件時,IDE會自動地將合適條件的方法列表展現給開發者,供開發者使用。
在上面將Code屬性表的時候也講了,若是編譯的第三方包,沒有LocalVariableTable屬性表信息,IDE的提示信息會稍有不一樣:
以上就是Class文件的方法表集合的所有內容。
讀者可能以爲本文關於方法表的Code屬性表討論的不夠深刻,在討論Code屬性表的時候,我簡單介紹了它的兩個屬性表LineNumberTable 和LocalVariableTable這兩個在有什麼實際做用,可是沒有詳細第介紹它們,而且在列舉的例子中,刻意地使用了 -g:none 選項 ,以使生成的class文件沒有這兩項信息,這麼作是由於Code 屬性太過複雜,而本文主要是想讓讀者瞭解的是 方法表集合,因此就生成了最精簡的Code屬性表,以減小讀者的負擔。
接下來的一篇文章,我打算專門來討論Code屬性表,揭開Code屬性表的全部祕密,敬請關注~~
本文還引出了一個須要討論的話題:就是Code屬性表中的機器指令,機器指令的運行要依賴於JVM體系結構的設計機制,理解機器指令的運行機制,這將是根很是很是難啃的骨頭.......
本文源自 http://blog.csdn.net/luanlouis/