1、字段表
字段表緊隨在接口表索引之後,字段表包含訪問標記、字段名索引、描述符索引、屬性表,其中屬性表包含屬性計數器與屬性集合
以這段代碼爲例:
package com.yang.testField;
public class Main {
private volatile int a = 1;
public static final String b = "abc";
}
16進制數據如下圖所示:
可以看得出,字段計數爲0x0002,因爲有2個字段,a和b。
字段a的訪問標記是是0x0042,用這個值與標識符的特徵值取與,如果結果爲1,則表示該字段擁有響應的標識符。字段標識符如下所示:
訪問標記名 |
十六進制值 |
描述 |
ACC_PUBLIC |
0x0001 |
public |
ACC_PRIVATE |
0x0002 |
private |
ACC_PROTECTED |
0x0004 |
protected |
ACC_STATIC |
0x0008 |
static |
ACC_FINAL |
0x0010 |
final |
ACC_VOLATILE |
0x0040 |
volatile |
ACC_TRANSIENT |
0x0080 |
transient,被transient 修飾的字段默認不會被序列化 |
ACC_SYNTHETIC |
0x1000 |
表示這個字段是由編譯器自動生成,而不是用戶代碼編譯產生 |
ACC_ENUM |
0x4000 |
枚舉 |
這裏我們可以得出,a的訪問標記有ACC_PRIVATE與ACC_VOLATILE。
a的名稱索引爲0x0005,我們看一下常量池:
可以得出第一個字段的名稱索引指向常量池中第5個常量項,即“a”。
a的描述符索引爲0x0006,即常量池中的“I”,完成的字段類型與描述符的對照表如下:
描述符 |
類型 |
B |
byte |
C |
char |
D |
double |
F |
float |
I |
int |
J |
long |
S |
short |
Z |
bool |
LClassName ; |
引用類型,"L" + 對象類型的全限定名 + ";" |
[ |
一維數組,N維數組就有N個連續的[ |
接下來是a的屬性計數器,對應的值爲0x0000,代表a沒有屬性表。
貼一下b字段表中的屬性表:
b的屬性計數器爲0x0001,代表着有屬性表,屬性表中只有一個元素,爲0x0009,常量池中顯示爲ConstantValue,說明
該屬性是ConstantValue類型的,屬性長度爲2,屬性值索引爲0x000A,即找到常量池中的#11,再找到#21,原來是個字符串"abc"。
爲什麼int a沒有屬性表,而static final b卻有屬性表?這要從字段的賦值策略說起:
對於一個實例字段,比如這裏的a,賦值階段發生在對象實例的構造方法中,即<init>;
對於一個非final的靜態字段,賦初始值會發生在解析階段,而賦用戶指定的值,會發生在初始化階段,在類構造器方法中完成,即<clinit>。
對於一個final的靜態字段,且是基本類型或者是String類型,在編譯期間就給該變量賦予用戶指定的值,並在常量池中形成一個ConstantValue類型的屬性,屬性值就是常量的值。如果是除去String類型以外的引用類型,那麼就是在初始化階段完成賦值操作。
下面以一個例子說明:
package com.yang.testField;
public class Main {
private volatile int a = 1;
public static final String b = "abc";
public static String c="def";
public static Thread d=new Thread();
}
<init>方法內的情況:
這裏面完成的是對實例變量的賦值操作。
<clinit>方法內的情況:
這裏面完成的是對普通靜態變量c與非String的引用類型變量d的賦值操作。
更多關於對<init>與<clinit>方法的理解,可以參考這篇文章java執行順序之深入理解clinit和init
2、方法表
緊接着字段表的是方法表,方法表和字段表類似,方法表包含方法計數、訪問標記、名稱索引、描述符索引、屬性表,其中屬性表也是包含屬性計數與屬性集合。
方法計數、名稱索引這邊就不再說明了。
方法的訪問標記有:
方法訪問標記 |
特徵值 |
描述 |
ACC_PUBLIC |
0x0001 |
public |
ACC_PRIVATE |
0x0002 |
private |
ACC_PROTECTED |
0x0004 |
protected |
ACC_STATIC |
0x0008 |
static |
ACC_FINAL |
0x0010 |
final |
ACC_SYNCHRONIZED |
0x0020 |
synchronized |
ACC_BRIDGE |
0x0040 |
bridge 方法, 由編譯器生成 |
ACC_VARARGS |
0x0080 |
方法包含可變長度參數,比如 String... args |
ACC_NATIVE |
0x0100 |
native |
ACC_ABSTRACT |
0x0400 |
abstract |
ACC_STRICT |
0x0800 |
聲明爲 strictfp,表示使用 IEEE-754 規範的精確浮點數,極少使用 |
ACC_SYNTHETIC |
0x1000 |
表示這個方法是由編譯器自動生成,而不是用戶代碼編譯產生 |
這裏有一個簡單的例子:
package com.yang.testMethod;
public class Main {
public Main() {
}
private int getInt(int k) {
return k;
}
public static Thread getThread(int i, double d, Runnable runnable) {
System.out.println(i * d);
return new Thread(runnable);
}
}
構造方法的描述符爲()V
getInt方法的描述符爲(I)I
getThread方法的描述符爲(IDLjava/lang/Runnable;)Ljava/lang/Thread;
從這裏,我們可以看得出,方法描述符的組織方式是這樣子的:(參數列表內字段的描述符)返回值的描述符
接下來討論方法的屬性表,前面說過了,屬性表包含屬性計數與屬性集合,屬性集合又包含屬性名稱索引+屬性長度+屬性值。
屬性表內最主要的屬性就是Code屬性了,Code屬性內有幾個比較重要的東西:字節碼、LineNumberTable行號表、LocalVariableTable局部變量表、ExceptionTable異常表
用一下的代碼爲例:
public static Thread getThread(int i, double d, Runnable runnable) {
try {
System.out.println(i * d);
}catch (Exception e){
return null;
}
return new Thread(runnable);
}
字節碼是class文件中最重要的東西了,jvm主要就是抽取字節碼,然後去執行。
LineNumberTable內維護這java源碼與字節碼之間的對應關係:
LocalVariableTable內記錄着局部變量描述:
關於局部變量表的詳細內容,可以參考我的另外一篇文章虛擬機棧的五臟六腑。
ExceptionTable會告訴虛擬機異常的處理邏輯,比如下圖的異常表,說明如果字節碼從第0行到第10行出現了type類型的異常,那麼將會跳轉到第13行的字節碼進行處理。