【JVM虛擬機】(8)--深入理解Class中--方法、屬性表集合


深入理解Class中--方法、屬性表集合

之前有關class文件已經寫了兩篇博客:

1、【JVM虛擬機】(5)---深入理解JVM-Class中常量池

2、【JVM虛擬機】(6)---深入理解Class中訪問標志、類索引、父類索引、接口索引

3、【JVM虛擬機】(7)---深入理解Class中-屬性集合

那么這篇博客主要講有關 方法表集合 相關的理解和代碼示例。

方法表集合: 告知該方法是什么修飾符修飾?是否有方法值?返回類型是什么?方法名稱,方法參數,還有就是方法內的一些信息。

一、方法集合概念

1、概念

方法表集合:方法表集合和屬性表集合其實很相似,都是由一個計數器(方法)若干個方法表構成,只不過方法表的結構相對復雜很多。

方法表的結構體訪問標志(access_flags)、名稱索引(name_index)、描述索引(descriptor_index)、屬性表(attribute_info)集合組成。

method_info {
                    u2 access_flags;
                    u2 name_index;
                    u2 descriptor_index;
                    u2 attributes_count;
                    attribute_info attributes[attributes_count];
             }

1)、訪問標志

不多說了,和屬性中的其實差不多,只是有些修飾符不一樣。

2)、名稱索引

就是指這個方法的名稱。如:'public void getXX()'中,getXX就是名稱索引。名稱索引兩個字節,這個方法的名稱以UTF-8格式的字符串存儲在這個常量池項中。

3)、描述索引

指這個方法的返回值,方法內參數信息。一個方法的描述包含若干個參數的數據類型和返回值的數據類型。

4)、屬性表(attribute_info)集合

下面講

二、屬性表集合

1、概述

在Class文件、字段表、方法表都可以攜帶自己的屬性表集合,用於描述某些場景專有的信息。
在方法表中, 屬性表集合記錄了某個方法的一些屬性信息,這些信息包括:

  • 這個方法的代碼實現,即方法的可執行的機器指令
  • 這個方法聲明的要拋出的異常信息
  • 這個方法是否被@deprecated注解表示
  • 這個方法是否是編譯器自動生成的

屬性表(attribute_info)結構體的一般結構如下所示:

屬性表占着非常大的一部分且定義了眾多屬性,上面只列舉了4個,查看完成的:JDK1.7版本中21項屬性表集合簡要介紹

下面介紹兩個重要的屬性

1、Code屬性

code屬性比較復雜,它是經過編譯器編譯成字節碼指令之后的數據。就是說java程序中的方法體經過javac編譯器處理后,最終變成字節碼存儲在Code屬性內

並非所有方法表都有這個屬性,接口和抽象類就沒有【沒有方法體】。
Code屬性是Class文件中最重要的一個屬性,在Class文件中,Code屬性用於描述代碼,所有的其它數據項目都用來描述元數據,了解code屬性對了解字
節碼執行引擎來說是必要基礎。

Code屬性表的組成部分:

機器指令——code

目前的JVM使用一個字節表示機器操作碼,即對JVM底層而言,它能表示的機器操作碼不多於28 次方,即 256個。class文件中的機器指令部分是class文件中最重要的部分,並且非常復雜。

異常處理跳轉信息

如果代碼中出現了try{}catch{}塊,那么try{}塊內的機器指令的地址范圍記錄下來,並且記錄對應的catch{}塊中的起始機器指令地址,當運行時在try塊中有異常拋出的話,JVM會將catch{}塊對應懂得其實機器指令地址傳遞給PC寄存器,從而實現指令跳轉;

Java源碼行號和機器指令的對應關系---LineNumberTable屬性表

編譯器在將java源碼編譯成class文件時,會將源碼中的語句行號跟編譯好的機器指令關聯起來,這樣的class文件加載到內存中並運行時,如果拋出異常,JVM可以根據這個對應關系,拋出異常信息,告訴我們我們的源碼的多少行有問題,方便我們定位問題。

Code屬性表結構體:

1、attribute_name_index : 屬性名稱索引,占有2個字節,其內的值指向了常量池中的某一項,該項表示字符. 串“Code”;

2、attribute_length : 屬性長度,占有 4個字節,其內的值表示后面有多少個字節是屬於此Code屬性表的;

3、max_stack : 操作數棧深度的最大值,占有 2 個字節,在方法執行的任意時刻,操作數棧都不應該超過這個值,虛擬機的運行的時候,會根據這個值來設置該方法對應的棧幀(Stack Frame)中的操作數棧的深度;

4、max_locals 最大局部變量數目,占有 2個字節,其內的值表示局部變量表所需要的存儲空間大小;

5、code_length : 機器指令長度,占有 4 個字節,表示跟在其后的多少個字節表示的是機器指令;

6、code 機器指令區域,該區域占有的字節數目由 code_length中的值決定。JVM最底層的要執行的機器指令就存儲在這里;

7、exception_table_length : 顯式異常表長度,占有2個字節,如果在方法代碼中出現了try{} catch()形式的結構,該值不會為空,緊跟其后會跟着若干個exception_table結構體,以表示異常捕獲情況;

8、exception_table : 顯式異常表,占有8 個字節,start_pc,end_pc,handler_pc中的值都表示的是PC計數器中的指令地址。exception_table表示的意思是:如果字節碼從第start_pc行到第end_pc行之間出現了catch_type所描述的異常類型,那么將跳轉到handler_pc行繼續處理。

9、attribute_count : 屬性計數器,占有 2 個字節,表示Code屬性表的其他屬性的數目

10、attribute_info : 表示Code屬性表具有的屬性表,它主要分為兩個類型的屬性表:“LineNumberTable”類型和“LocalVariableTable”類型。“LineNumberTable”類型的屬性表記錄着Java源碼和機器指令之間的對應關系“LocalVariableTable”類型的屬性表記錄着局部變量描述

2、ConstantValue屬性

之所以學習這個,是因為后面類加載機制有聯系到這個屬性

這個屬性的作用是通知虛擬機為靜態變量賦值,只要被static修飾的變量才有這個屬性,【有該屬性的字段必須有ACC_STATIC訪問標志,反過來不一定】。
對於 "int x = 123" 和 "static int x =123"這類代碼在日常編寫中很常見,但虛擬機對這兩種變量賦值的時刻卻不同。
對於非static變量[實例變量],是在實例構造器<init>進行
對於類變量,有兩種方式選擇
①在類構造器<clinit>方法中賦值
②使用ConstantValue屬性初始化
目前Sun javac編譯器是這么做的【具體咋做不知道 = =】,如果同時使用final和static修飾一個變量[這種修飾就相當於個常量],並且是String或基本類型,就使用②,
如果沒有被final修飾或不是基本類型和String,就選擇①在<clinit>方法中初始化
//有關這點我在上篇博客舉過例子,最后幾句話也對這個解釋的很清楚。

三、示例

有關方法的代碼示例,我就不親自測了,因為有位博主寫的已經很清晰啦,我自己寫也沒那么清晰。

1、訪問標志

public static synchronized final void greeting(){  
}  

greeting()方法的修飾符有:public、static、synchronized、final 這幾個修飾符修飾,那么相對地,

greeting()方法的訪問標志中的ACC_PUBLICACC_STATICACC_SYNCHRONIZEDACC_FINAL標志位都應該是1

從上面第一張圖可以得出,該訪問標志的值應該是十六進制0x0039

2、名稱索引和描述符索引

緊接着訪問標志(access_flags)后面的兩個字節,叫做名稱索引(name_index),這兩個字節中的值是指向了常量池中某個常量池項的索引,該常量池項表示這這個方法名稱的字符串。

方法描述符索引(descrptor_index)是緊跟在名稱索引后面的兩個字節,這兩個字節中的值跟名稱索引中的值性質一樣,都是指向了常量池中的某個常量池項。這兩個字節中的指向的常量池項,是表示了方法描述符的字符串

3、代碼示例

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文件中會顯示有兩個方法呢?

原因:如果我們在類中沒有定義實例化構造方法,JVM編譯器在將源碼編譯成class文件時,會自動地為這個類添加一個不帶參數的實例化構造方法,這種添加是字節碼級別的,JVM對所有的類實例化構造方法名采用了相同的名稱:“ ”。如果我們顯式地如下定義Simple()構造函數,這個類編譯出來的class文件和上面的不帶Simple構造方法的Simple類生成的class文件是完全相同的。

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;

解釋下機器指令集:

第一個字節 **0x2A:查詢Java 虛擬機規范中關於操作碼的解釋,0x2A 對應的操作是"aload_0",作用是將第一個引用類型局部變量推送至棧頂;

第二個字節 0xB7 :0xB7 對應的操作是:"invokespecial",作用是調用超類構造方法、實例初始化方法或私有方法;它****帶有2個字節的參數,即后面的 0x00、0x01 是它的參數,這個參數是某個常量池中的索引,指向了常量池的第一項,該項表示一個方法引用項CONSTANT_Methodref_info結構體,表示java.lang.Object 類中的<init>()方法,即 java/lang/Object."<init>"😦)V。這條指令的意思就是調用父類Object的構造方法<init>()

第5個字符是0xB1 : 對應操作是:“Ireturn”,作用是表示無返回值的方法返回,結束方法調用,這條語句放在方法的機器碼最后,表示方法結束調用,返回。

我們可以使用javap -v Simple > Simple.txt,查看反編譯信息是怎樣顯示這一信息的:

3)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;

指令集解釋

第一個字節 0x10 : 查詢Java虛擬機規范中關於操作碼的解釋,0x10 對應的操作是"bipush"," 作用是將單字節的常量值(-128~127) 推送至棧頂,它要求一個參數,后面的 0x0A 即是需要推送到棧頂的單字節,注意這里的 0x0A 是16進制,就是我們在代碼里寫的"a=10"中的10。

第三個字節"3B" : “3B”對應的操作是:"istore_0",作用是將棧頂int 型數值存入第一個局部變量。我們在greeting() 方法中就聲明了一個局部變量a,JVM的運行的時候,將這個局部變量a解析,並放置到局部變量表中的第一個位置;上述的0x10 0x0A 指令已經將0x0A 推送到了棧頂了,然后 0x3B指令便將棧頂的0x0A 取出,賦值給局部變量表中的第一個參數,即局部變量a,這樣就完成了對局部變量a的賦值;

第4個字符是0xB1 : 對應操作是:“Ireturn”,作用是表示無返回值的方法返回,結束方法調用,這條語句放在方法的機器碼最后,表示方法結束調用,返回。

我們可以使用javap -v Simple > Simple.txt,查看反編譯信息是怎樣顯示這一信息的:


參考

這篇文章基本上是參考,非常感謝作者分享,寫的很清楚:《Java虛擬機原理圖解》



只要自己變優秀了,其他的事情才會跟着好起來(少將6)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM