1. 程式人生 > >深入理解Java Class檔案格式(九)

深入理解Java Class檔案格式(九)

經過前八篇關於class檔案的部落格, 關於class檔案格式的內容也基本上講完了。 本文是關於class檔案格式的最後一篇。 在這篇部落格中, 將會講解關於方法的幾個屬性。 理解這篇部落格的內容, 對於理解JVM執行引擎起著重要作用。 關於虛擬機器執行引擎有關的內容, 會在本專欄後面的部落格中涉及。 

在前面幾篇部落格中, 我們知道在class檔案中描述一個方法, 會使用一個method_info 。 這個method_info中存放了方法的修飾符標誌位,還引用了常量池中的項, 這些常量池資料項描述了在當前類中定義的某個方法的方法名, 方法的描述符。 關於這部分的內容, 請參考我之前的部落格:深入理解Java Class檔案格式(七)  。 

但是method_info中並沒有存放方法的位元組碼, 也就是指令。 我們知道, 對於一個方法來說, 只要它不是抽象的(抽象類中的抽象方法或者介面中的方法), 那麼肯定就會存在指令。 那麼這些指令存放在哪裡呢? 還有, 方法中的異常處理器(try-catch塊)是如何在class檔案中表述的? 方法宣告丟擲的異常是如何描述的呢? 如果你對這幾個問題感興趣, 或許你會在這篇部落格中找到答案, 或者受到一些啟發。 

為了知識的連貫性, 我們首先簡單回顧一下method_info的結構, 因為method_info與本文有著密切的關係。method_info 的結構如下:

Code屬性

code屬性是方法的一個最重要的屬性。 因為它裡面存放的是方法的位元組碼指令, 除此之外還存放了和運算元棧,區域性變數相關的資訊。 所有不是抽象的方法, 都必須在method_info中的attributes中有一個Code屬性。下面是Code屬性的結構, 為了更直觀的展示Code屬性和method_info的包含關係, 特意畫出了method_info:

下面依次介紹code屬性中的各個部分。

和上一篇部落格中介紹的其他屬性一樣,attribute_name_index指向常量池中的一個CONSTANT_Utf8_info , 這個CONSTANT_Utf8_info 中存放的是當前屬性的名字 “Code” 。

attribute_length給出了當前Code屬性的長度(不包括前六位元組)。

max_stack 指定當前方法被執行引擎執行的時候, 在棧幀中需要分配的運算元棧的大小。

max_locals指定當前方法被執行引擎執行的時候, 在棧幀中需要分配的區域性表量表的大小。注意, 這個數字並不是區域性變數的個數, 因為根據區域性變數的作用域不同, 在執行到一個區域性變數以外時, 下一個區域性變數可以重用上一個區域性變數的空間(每個區域性變數在區域性變量表中佔用一個或兩個Slot)。 方法中的區域性變數包括方法的引數, 方法的預設引數this, 方法體中定義的變數, catch語句中的異常物件。 關於執行引擎的相關內容會在後面的部落格中講到。

code_length指定該方法的位元組碼的長度, class檔案中每條位元組碼佔一個位元組。

code存放位元組碼指令本身, 它的長度是code_length個位元組。

exception_table_length 指定異常表的大小

exception_table就是所謂的異常表, 它是對方法體中try-catch_finally的描述。 exception_table可以看做是一個數組, 每個陣列項是一個exception_info結構, 一般來說每個catch塊對應一個exception_info,編譯器也可能會為當前方法生成一些exception_info。 exception_info的結構如下(為了直觀的顯示exception_info, exception_table和Code屬性的關係, 畫出了Code屬性,的話讀者就會更清楚各個資料項之間的位置關係和包含關係):

下面講解exception_info中的各個欄位的意思。

start_pc是從位元組碼(Code屬性中的code部分)起始處到當前異常處理器起始處的偏移量。

end_pc是從位元組碼起始處到當前異常處理器末尾的偏移量。

handler_pc是指當前異常處理器用來處理異常(即catch塊)的第一條指令相對於位元組碼開始處的偏移量。

catch_type是一個常量池索引, 指向常量池中的一個CONSTANT_Class_info資料項, 該資料項描述了catch塊中的異常的型別資訊。這個型別必須是java.lang.Throwable的或其子類。

所以可以總結, 一個異常處理器(exception_info)的意思是: 如果偏移量從start_pc到end_pc之間的位元組碼出現了catch_type描述的型別的異常, 那麼就跳轉到偏移量為handler_pc的位元組碼處去執行。如果catch_type為0, 就代表不引用任何常量池項(再回顧一下, 常量池中的項是從1開始計的), 那麼這個exception_info用於實現finally子句。

我們一直在介紹Code屬性, 只不過剛才進行了一個小插曲, 介紹了Code屬性中的exception_table中的exception_info的詳細資訊。 下面我們繼續介紹Code 屬性中的其他資訊, 希望讀者不要被繞暈了 : )

attributes_count 表示當前Code 屬性中存在的其他屬性的個數。 現在我們知道, class中的屬性, 不僅會出現在頂層的class中, 會存在field_info中, 會存在method_info中, 甚至還會出現在屬性中。 
attributes可以看做是一個數組, 裡面存放了Code屬性中的其他屬性。 Code 屬性中可以出現的其他屬性有LineNumberTable和LocalVariableTable 。 這兩個屬性會在下面介紹。

LineNumberTable屬性

LineNumberTable屬性存在於Code屬性中, 它建立了位元組碼偏移量到原始碼行號之間的聯絡。 這個屬性是可選的, 編譯器可以選擇不生成該屬性。下面是該屬性的結構(同樣給出了全域性的位置關係,LineNumberTable在圖的右下角部分):

由於這個屬性並不是重點, 我們在此簡單的講述。 

每個LineNumberTable中的line_number_table部分, 可以看做是一個數組, 陣列的每項是一個line_number_info , 每個line_number_info 結構描述了一條位元組碼和原始碼行號的對應關係。 其中start_pc是這個line_number_info 描述的位元組碼指令的偏移量, line_number是這個line_number_info 描述的位元組碼指令對應的原始碼中的行號。可以看出, 方法中的每條位元組碼都對應一個line_number_info , 這些line_number_info 中的line_number可以指向相同的行號, 因為一行原始碼可以編譯出多條位元組碼。

LocalVariableTable屬性 

LocalVariableTable 屬性建立了方法中的區域性變數與原始碼中的區域性變數之間的對應關係。 這個屬性存在於Code屬性中。 這個屬性是可選的, 編譯器可以選擇不生成這個屬性。該屬性的結構如下:(同樣給出了全域性的位置關係圖,LocalVariableTable 在該圖的右下角 )

由於這個屬性相對不那麼重要, 這裡只是大概講解一下。

每個LocalVariableTable 的local_variable_table部分可以看做是一個數組, 每個陣列項是一個叫做local_variable_info的結構, 該結構描述了某個區域性變數的變數名和描述符, 還有和原始碼的對應關係。下面講解local_variable_info的各個部分:

start_pc是當前local_variable_info所對應的區域性變數的作用域的起始位元組碼偏移量;

length是當前local_variable_info所對應的區域性變數的作用域的大小。 也就是從位元組碼偏移量start_pc 到start_pc+length就是當前區域性變數的作用域範圍;

name_index指向常量池中的一個CONSTANT_Utf8_info, 該CONSTANT_Utf8_info描述了當前區域性變數的變數名;

descriptor_index指向常量池中的一個CONSTANT_Utf8_info, 該CONSTANT_Utf8_info描述了當前區域性變數的描述符;

index描述了在該方法被執行時,當前區域性變數在棧幀中區域性變量表中的位置。 

由此可知, 方法中的每個區域性變數都會對應一個local_variable_info 。

Exceptions屬性

首先需要說明, Exceptions屬性不是存在於Code屬性中的, 它存在於method_info中的attributes中。 和Code屬性是平級的。 這個屬性描述的是方法宣告的可能會丟擲的異常, 也就是方法定義後面的throws宣告的異常列表, 請不要和上面提到的異常處理器混淆。 異常處理器描述了方法的位元組碼如何處理異常, 而Exceptions屬性描述方法可能會丟擲哪些以異常。下面講解Exceptions屬性的結構(左下角為Exceptions屬性):

下面講解Exceptions屬性中的資訊。 

attribute_name_index和attribute_length就不多說了, 和其他屬性是一樣的。 

number_of_exceptions是該方法要丟擲的異常的個數。 

exceptions_index_table可以看做一個數組, 這個陣列中的每一項佔兩個位元組, 這兩個位元組是對常量池的索引, 它指向一個常量池中的CONSTANT_Class_info。 這個CONSTANT_Class_info描述了一個被丟擲的異常的型別。 


總結
到此為止, 和方法相關的屬性就介紹完了。 這篇部落格講解的內容相對比較複雜。 下面以一個例項進行驗證, 例項程式碼:
 

package com.jg.zhang;
 
public class Test {
 
	public void test() throws Exception{
		
		int localVar = 0;
		
		try{
			
			Class.forName("com.jg.zhang.Person");
			
		}catch(ClassNotFoundException e){
			
			throw e;
		}finally{
			System.out.println(localVar);
		}
		
	}
}

反編譯後的test方法部分(省略了常量池等資訊):

  public void test() throws java.lang.Exception;
    flags: ACC_PUBLIC
    Exceptions:
      throws java.lang.Exception
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: ldc           #18                 // String com.jg.zhang.Person
         4: invokestatic  #20                 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
         7: pop
         8: goto          24
        11: astore_2
        12: aload_2
        13: athrow
        14: astore_3
        15: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_1
        19: invokevirtual #32                 // Method java/io/PrintStream.println:(I)V
        22: aload_3
        23: athrow
        24: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;
        27: iload_1
        28: invokevirtual #32                 // Method java/io/PrintStream.println:(I)V
        31: return
      Exception table:
         from    to  target type
             2     8    11   Class java/lang/ClassNotFoundException
             2    14    14   any
      LineNumberTable:
        line 7: 0
        line 11: 2
        line 13: 8
        line 15: 12
        line 16: 14
        line 17: 15
        line 18: 22
        line 17: 24
        line 20: 31
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      32     0  this   Lcom/jg/zhang/Test;
               2      30     1 localVar   I
              12       2     2     e   Ljava/lang/ClassNotFoundException;
 

結合上面的講解和圖解, 再分析反編譯的結果, 就一目瞭然了: 所有的結果是一個method_info, method_info開始處是訪問標誌資訊。 然後是method_info的 Exceptions屬性 , Exceptions屬性屬性下面是Code屬性, Code屬性中又包括位元組碼, 異常處理器 ,LineNumberTable屬性和LocalVariableTable 屬性。 

由於這篇部落格講解的內容大多和方法有關, 所以會直接或間接的和method_info有聯絡, 最後給出一張全域性圖, 這樣的話, 讀者就比較明確, 一個完整的方法, 是如何在class檔案中描述的,由於考慮到複雜性, 這些屬性或其他資料項中, 對常量池的引用均未畫出: