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

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

在本專欄的第一篇文章 深入理解Java虛擬機器到底是什麼 中, 我們主要講解了什麼是虛擬機器, 這篇部落格是對JVM的一個概述。 在隨後的幾篇文章中,一直在講解class檔案格式。 在今天這篇部落格中, 將會繼續講解class檔案中的其他資訊。 在本文中, 將會講解class檔案中的最後一部分, 屬性(attributes) 。 這裡的屬性和原始檔中的屬性不是一個概念。 在原始檔中, 我們把在類中定義的欄位也叫做屬性。 而class檔案中的屬性, 可以看做是儲存一些額外資訊的資料結構。 下面我們就來介紹屬性。

class檔案中的attributes_count和attributes

attributes_count位於class檔案中methods的下面。 它佔兩個位元組, 儲存的是一個整數值, 表示class檔案中屬性的個數。 

attributes_count下面的是attributes, 可以把它看做一個數組, 每個陣列項是一個attribute_info , 每個attribute_info 表示一個屬性。attributes中有 attributes_count個attribute_info 。

需要說明的是, 屬性會出現在多個地方, 不僅僅出現在頂層的ClassFile中, 也會出現在class檔案中的資料項中, 如出現在field_info中, 用來描述特定欄位的一些資訊, 還可以出現在method_info中, 用來描述特定方法的一些資訊。 (關於field_info和method_info已經在上面一篇部落格中介紹過, 不明白的可以參考上面的部落格: 深入理解Java Class檔案格式(七))

屬性(attribute_info)的大概格式是這樣的:

其中attribute_name_index佔兩個位元組, 它是一個指向常量池資料項的索引。 它指向一個CONSTANT_Utf8_info , 這個CONSTANT_Utf8_info 中存放的是當前屬性的名字。

attribute_name_index下面的四個位元組叫做attribute_length, 它表示當前屬性的長度, 這個長度不包括前6個位元組, 也就是說只包括屬性真實資訊(也就是info)的長度

attribute_length下面的資料是info, 它的長度由上面提到的attribute_length指定, 它存放的是真實的屬性資料。

下面我們會依次介紹一些重要屬性, 相對不是很重要的屬性會一筆帶過。


ClassFile中的SourceFile屬性

首先介紹一個比較簡單的屬性:SourceFile。 該屬性出現在頂層的class檔案中。 它描述了該類是從哪個原始檔中編譯來的, 注意, 描述的是原始檔, 而不是類, 一個原始檔中可以存在多個類。 它的格式如下:

前面說過, attribute_name_index指向常量池中的一個CONSTANT_Utf8_info , 這個CONSTANT_Utf8_info 中存放的是這個屬性的名字字串, 即“SourceFile” 。 

attribute_length是屬性資訊的長度, 這裡是2, 因為這個屬性的info就兩個位元組。

sourcefile_index佔兩個位元組, 這也是為什麼attribute_length是2的原因。 sourcefile_index指向常量池中的一個CONSTANT_Utf8_info , 這個CONSTANT_Utf8_info 中存放的是生成該類的原始檔的檔名, 這裡的檔名不包括路徑部分。

下面舉例說明, 示例程式碼:
 

package com.jg.zhang;
 
public class Person {
 
	int age;
 
	int getAge(){
		return age;
	}
}

反編譯後的相關資訊:

public class com.jg.zhang.Person
  SourceFile: "Person.java"
Constant pool:
.........
  #20 = Utf8               SourceFile
  #21 = Utf8               Person.java
 
.........
 

 

反編譯結果中的  SourceFile: "Person.java"  一行是SourceFile屬性的簡單表示形式。 可以把它看做一個可讀的attribute_info 。 下面常量池中的第20項的CONSTANT_Utf8_info是對這個屬性的屬性名(attribute_name_index)的描述 , 第21項的CONSTANT_Utf8_info是對原始檔的檔名的描述。

下面是圖例, 注意, 虛線範圍內表示常量池區域:

ClassFile中的InnerClasses屬性

InnerClasses是一個存在於頂層class檔案中的屬性, 它描述的是內部類和外圍類的關係。  這是一個相對來說比較複雜的屬性, 因為每個類可能有多個內部類, 而這些內部類中可能還有內部類, 多層巢狀。外圍類中的InnerClasses屬性必須描述它的所有內部類, 而內部類中的InnerClasses也必須描述它的外圍類。 

由於這個屬性相對較為複雜, 而對於我們理解class檔案又不具有很大的意義, 所以我們只是簡單的介紹一下。 如果想深入理解這個屬性, 請參考 《深入Java虛擬機器》 第144到166頁。 

下面是這個屬性的結構:

attribute_name_index和attribute_length就不過多介紹了, 和上面介紹的是一樣的。

number_of_classes描述的是內部類的個數。

classes可以看做是一個數組, 這個陣列中的每一項是一個inner_class_info, 而每個inner_class_info是對一個內部類的描述。每個 inner_class_info的結構如下:

Synthetic屬性

Synthetic屬性可以出現在filed_info中, method_info中和頂層的ClassFile中, 分別表示這個欄位, 方法或類不是有使用者程式碼生成的(即不存在與原始檔中), 而是由編譯器自動新增的。 例如, 編譯器會為內部類增加一個欄位, 該欄位是對外部類物件的引用; 如果一個不定義構造方法, 那麼編譯器會自動新增一個無引數的構造方法<init>, 如果定義了靜態欄位或靜態程式碼塊, 還會根據具體情況, 增加靜態初始化方法<clinit> 。 此外, 有些機制, 如動態代理, 會在執行時自動生成位元組碼檔案, 由於這些類不是由原始檔中編譯來的, 所以這些類的class檔案中會有一個Synthetic屬性。 

它的結構如下:

可以看到, 它沒有真正的屬性資料info, 它只是一個標誌性的屬性, 用來表示它所在的欄位, 方法或類是由編譯器自動新增的 。 

下面以例項程式碼來說明, 原始碼如下:

package com.jg.zhang;
 
public class Person {
	
	static{
		System.out.println("static");
		
	}
	
	int age;
 
	int getAge(){
		return age;
	}
}

反編譯後的相關資訊如下:

{
  int age;
    flags:
 
 
  static {};
 
    .........
 
  public com.jg.zhang.Person();
    
    .........
 
  int getAge();
 
    .........
}

由反編譯結果可以看出, 編譯器自動生成了靜態初始化方法和構造方法。 可能是因為Synthetic屬性是可選的(也就是說某個版本的編譯器可以選擇不加入Synthetic屬性) ,所以在反編譯後的結果中沒有發現Synthetic屬性。

ConstantValue屬性

ConstantValue屬性出現在class檔案中的field_info中, 也就是說它是一個和欄位相關的屬性。 每個field_info中最多隻能出現一個ConstantValue屬性。 此外, 要注意的是, 必須是靜態欄位才可以有ConstantValue屬性。 這個靜態欄位可以是final的, 也可以不是final的。 

這個屬性為靜態變數提供了另一種初始化的方式。 靜態變數初始化的方式有兩種, 一種就是現在要講得ConstantValue屬性, 另一種就是靜態初始化方法<clinit> 不同的編譯器和虛擬機器可以有不同的實現方式。 但是如果虛擬機器決定使用ConstantValue屬性為靜態變數賦值, 那麼為這個變數的賦值動作, 必須位於執行<clinit>方法之前。 

此外, 只有基本資料型別或String型別的靜態變數才可以存在ConstantValue屬性, 原因在下面會有說明。 

下面介紹它的結構:

attribute_name_index和attribute_length就不過多介紹了, 和上面介紹的是一樣的。這裡的attribute_length為2 。 

位於attribute_length之下的是constantvalue_index , 這是一個指向常量池中某個資料項的索引。這個常量池資料項中存放的就是當前欄位的值。

 這個常量池中的資料項,根據field_info描述的欄位的不同, 可以是不同型別的資料項, 如果當前欄位是byte, short, char, int, boolean型別, 那麼這個被指向的常量池資料項就會是一個CONSTANT_Integer_info , 如果當前欄位是一個long型別的欄位, 那麼這個被指向的常量池資料項就會是一個CONSTANT_Long_info 。 如果當前欄位是是一個String型別的欄位 , 那麼這個被指向的常量池資料項就是一個CONSTANT_String_info 。 這裡有一點需要說明, 雖然java語言支援byte, short, char, boolean型別, 但是JVM卻不支援這幾種型別, 表現在class檔案中就是, class檔案中的常量池中沒有和這幾個資料型別相對應的資料項, 這幾中型別都被JVM在執行時當做int來對待, 表現在class檔案中就是, 這幾種型別都對應常量池中的CONSTANT_Integer_info 資料項。 

這也說明了, 為什麼只有基本資料型別和String型別的靜態常量才會存在ConstantValue屬性 。 因為constantvalue_index只是一個指向常量池的索引, 而其他引用型別的常量不會存在於常量池中。

下面以例項來說明, 例項程式碼如下:
 

package com.jg.zhang;
 
public class Person {
	
	static final int a = 1;
	
	int age;
 
	int getAge(){
		return age;
	}
}

反編譯後的相關結果如下:

......
 
Constant pool:
 
   #7 = Utf8               ConstantValue
   #8 = Integer            1
 
 
{
  static final int a;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 1
 
    .........
}
 

 

可以看到, 原始檔中的a欄位, 是static final 的, 所以編譯器為這個欄位的filed_info生成了ConstantValue屬性。 這個屬性的示意圖如下所示, 注意, 虛線範圍內表示常量池區域:

Deprecated屬性

Deprecated屬性可以存在於filed_info中, method_info中和頂層的ClassFile中, 分別表示這個欄位, 方法或類已經過時。 這個屬性用來支援原始檔中的@deprecated註解。 也就是說, 如果在原始檔中為一個欄位, 方法或類標註了@deprecated註解, 那麼編譯器就會在class檔案中為這個欄位, 方法或類生成一個Deprecated屬性 。

Deprecated屬性的格式如下:

上面的屬性一樣, attribute_name_index屬性指向一個常量池中的CONSTANT_Utf8_info 。 這個CONSTANT_Utf8_info中存放著該屬性的名字 “Deprecated” 。 

attribute_length永遠為0 , 因為這個屬性只是一個標誌資訊, 用來表示欄位, 方法, 類已經過時, 而不具有任何實質性的屬性資訊。

下面以程式碼示例來說明, 程式碼如下:
 

package com.jg.zhang;
 
public class Person {
	
	int age;
 
	@Deprecated
	int getAge(){
		return age;
	}
}

在getAge方法上使用了@deprecated 。 下面是反編譯之後的相關資訊:

  ......
  
Constant pool:
  ......
 
  #18 = Utf8               Deprecated
 
  ......
 
{
 
  ......
 
  int getAge();
    flags:
    Deprecated: true
 
    ......
 
}

可以看到, 在getAge方法相關的資訊中, 有一行 Deprecated: true , 這說明編譯器在getAge方法的method_info中加入了Deprecated屬性。 常量池第18項的CONSTANT_Utf8_info中存放的是Deprecated屬性的屬性名“Deprecated” 。 

下面是示意圖, 虛線範圍內表示常量池區域:

總結

本文就到此為止。 在本文中, 主要講解了class檔案中的一些屬性。 這些屬性可以出現在class檔案中的對個地方, 用來描述一些其他資訊。 

在下一篇部落格中, 會繼續講解其他屬性。 下一篇部落格要講解的屬性相對比較重要, 因為這些屬性主要是和方法相關的。 到目前為止, 我們已經講解了class檔案中的大部分資訊, 包括常量池, this_class, super_class, field_info, method_info等等。 雖然method_info是對一個方法的描述, 但是目前我們知道的而關於method_info的資訊, 只描述了方法的方法名, 描述符等簽名信息。 但是方法中還包括很多重要資訊, 比如位元組碼指令, 異常處理塊, 方法宣告丟擲的異常 等。 這些重要資訊在class檔案中是如何描述的呢? 下一篇部落格將會揭曉答案, 敬請關注。