Java之所以能實現“Write Once, Run Anywhere”,是因為不同平台的虛擬機都統一使用一種程序存儲格式——字節碼。Java虛擬機不和包括Java在內的任何語言綁定,它只於“Class”文件這種特定的二進制文件格式所關聯。
Class文件是一組以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊排列在Class文件中,中間無任何分隔符。
明確兩個概念:無符號數和表
無符號數屬於基本的數據類型,以u1、u2、u4來分別代表1個字節、2個字節和4個字節的無符號數。
表是由多個無符號數或者其他表作為數據項構成的復合數據結構,整個Class文件本質上就是一張表。
類型 | 名稱 | 數量 | 描述 |
u4 | magic | 1 | 魔數 |
u2 | minor_version | 1 | 次版本號 |
u2 | major_version | 1 | 主版本號 |
u2 | constant_pool_count | 1 | 常量池容量 |
cp_info | constant_pool | costant_pool_count-1 | 常量池 |
u2 | access_flags | 1 | 訪問標志 |
u2 | this_class | 1 | 當前類常量索引 |
u2 | super_class | 1 | 超類常量索引 |
u2 | interfaces_count | 1 | 接口數量 |
u2 | interfaces | interfaces_count | 接口常量索引 |
u2 | fields_count | 1 | 字段數量 |
field_info | fields | fields_count | 字段信息 |
u2 | methods_count | 1 | 方法數量 |
method_info | methods | methods_count | 方法信息 |
u2 | attributes_count | 1 | 屬性數量 |
attribute_info | attributes | attributes_count | 屬性信息 |
解釋Class文件格式之前,先編寫一個簡單的java類
1 package com.yyl.Test; 2 public class Test{ 3 private int i = 2; 4 5 public int getResult(){ 6 return i + 2; 7 } 8 }
編譯成class文件后,用winhex軟件打開class字節碼
使用javap命令幫助分析
1、魔數(magic)
每個Class文件頭4個字節稱為魔數,它的唯一作用是確定這個文件能否為一個能為虛擬機接收的Class文件,基於安全考慮,使用魔數而不是擴展名來進行身份識別。從16進制字節碼中看出前4個字節為CAFEBABE(咖啡寶貝?)。
2、次/主版本號(minor_version/major_version)
緊接着魔數的4個字節分別是次版本號和主版本號,java的版本號是從45開始,高版本的JDK能向下兼容以前版本的Class文件,虛擬機拒絕執行超過其版本號的Class文件。從16進制字節碼中看出次版本號0x0000,主版本號0x0032。
3、常量池容量、常量池(constant_pool_count、constant_pool)
常量池主要存放兩大類常量:
a.字面值:接近java語言層面的常量概念,如文本字符串、final常量值等。
b.符號引用:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。
由於常量池中常量的數量是不固定的,所以在常量池入口需要設置一項u2類型數據,代表常量池容量計數值。其計數不是從0開始,而是從1開始。
如圖,常量池容量值為0x0013,即十進制19,因此容量為19-1=18個。查看javap命令輸出的常量表中也可以看出Constant pool總共有18個常量。
設計者把第0項空出來目的在於在特定情況下需要表達“不引用任何一個常量池項目”的含義。
常量池項目結構第一項均為u1類型的tag,該標志代表常量池項目的類型,而其他結構各異。
下面只列出部分結構:
常量 | 項目 | 類型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值為1 |
length | u2 | 占用字節數 | |
bytes | u1 | 長度為length的UTF-8編碼的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值為3 |
bytes | u4 | 按照高位在前存儲的int值 | |
CONSTANT_Class_info | teg | u1 | 值為7 |
index | u2 | 指向全限定名常量項的索引 | |
CONSTANT_String_info | tag | u1 | 值為8 |
index | u2 | 指向字符串字面值的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值為9 |
index | u2 | 指向聲明字段的類或接口描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引項 | |
CONSTANT_Methodref_info | tag | u1 | 值為10 |
index | u2 | 指向聲明方法的類描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向方法描述符CONSTANT_NameAndType_info的索引項 | |
CONSTANT_NameAndType_info | tag | u1 | 值為12 |
index | u2 | 指向該字段或方法名稱常量項的索引 | |
index | u2 | 指向該字段或方法描述符常量項的索引 |
接着分析字節碼:
如圖可以看出第一個常量項tag為0x0A,即十進制10,類型為CONSTANT_Methodref_info,接着的u2類型0x0004指向類的索引,即指向java/lang/Object類,后面的0x000F指向方法描述符,即指向方法名為<init>返回值為()V的描述符。所以整個常量項就表示如圖中的“結果”。
其他常量項類似上面方法,就不一一闡述。
另外,由於Class文件中方法、字段等都需要引用CONSTAN_Utf8_info型常量來描述名稱,所以該類型最大長度也就是java中方法、字段名的最大長度(u2類型表達的最大值為65535),所以如果定義了超過64KB英文字符的變量或方法名,將無法編譯。
4、訪問標志(access_flags)
常量池結束后,緊接着的兩個字節表示訪問標志,用於識別類或接口層次的訪問信息,包括這個Class是類還是接口,是否定義為public類型、abstract類型等等。
標志名稱 | 標志值 | 含義 |
ACC_PUBLIC | 0x0001 | 是否為public類型 |
ACC_FINAL | 0x0010 | 是否被聲明為final,只有類可設置 |
ACC_SUPER | 0x0020 | 是否允許使用invokespecial字節碼指令的新語義,JDK1.0.2之后編譯出來的類這個標志必須為真 |
ACC_INTERFACE | 0x0200 | 標識這是一個接口 |
ACC_ABSTRACT | 0x0400 | 是否為abstract類型,對於接口或抽象類來說此值為真,其他類值為假 |
ACC_SYNTHETIC | 0x1000 | 標識這個類並非由用戶代碼產生的 |
ACC_ANNOTATION | 0x2000 | 標識這個一個注解 |
ACC_ENUM | 0x4000 | 標識這是一個枚舉 |
Test類被public關鍵字修飾,因此它的ACC_PUBLIC、ACC_SUPER標志應當為真,因此其access_flags值應為0x0001|0x0020=0x0021。
5、類索引、父類索引與接口索引集合(this_class、super_class、interfaces)
類索引和父類索引都是一個u2類型的數據,接口索引集合是一組u2類型的數據的集合。它們各自指向一個類型為CONSTANT_Class_info的類描述符常量。
6、字段表集合(field_info)
字段表用於描述接口或類中聲明的變量,字段包括類級變量以及實例級變量,但不包括在方法內部聲明的局部變量。
類型 | 名稱 | 數量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段修飾符放在access_flags項目中,如下表
標志名稱 | 標志值 | 含義 |
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 |
ACC_SYNTHETIC | 0x1000 | 字段是否由編譯器自動產生 |
ACC_ENUM | 0x4000 | 字段是否為enum |
跟隨access_flags的標志是兩項索引值:name_index和descriptor_index,他們都是對常量池的引用,分別代表字段的簡單名稱以及字段和方法的描述符。
區分三個概念:全限定名、簡單名稱、描述符
a.全限定名,如java/lang/Object,僅僅將類全名中的“.”替換成“/”。
b.簡單名稱是指沒有類型和參數修飾的方法或者字段名稱,如類中getResult()方法和i字段的簡單名稱分別為“getResult”和“i”。
c.描述符是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。
標識字符 | 含義 | 標識字符 | 含義 |
B | 基本類型byte | J | 基本類型long |
C | 基本類型char | S | 基本類型short |
D | 基本類型double | Z | 基本類型boolean |
F | 基本類型float | V | 特殊類型void |
I | 基本類型int | L | 對象類型,如Ljava/lang/Object |
對於數組類型,每一唯獨使用一個前置的“[”字符描述,如一個定義為“java.lang.String[][]”類型的二維數組,將被記錄為“[[Ljava/lang/String”。
當描述符描述方法時,按照先參數列表后返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“()”之內。如int getResult()方法的描述符為“()I”。
字段表最后的屬性表結構可用於存儲一些額外的信息。
7、方法表集合(method_info)
方法表的結構跟字段表結構一樣,依次包括access_flags、name_index、descriptor_index、attributes,而訪問標志則有所區別。
標志名稱 | 標志值 | 含義 |
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 | 方法是否由編譯器產生的橋接方法 |
ACC_VARARGS | 0x0080 | 方法是否接收不定參數 |
ACC_NATIVE | 0x0100 | 方法是否為native |
ACC_ABSTRACT | 0x0400 | 方法是否為abstract |
ACC_STRICTFP | 0x0800 | 方法是否為strictfp |
ACC_SYNTHETIC | 0x1000 | 字段是否由編譯器自動產生 |
方法定義可以由訪問標志、名稱索引、描述符表達清楚,而方法里面的代碼經過編譯器編譯成字節碼指令后,存放在方法屬性表集合中一個名為“Code”的屬性里面。
8、屬性表集合(attribute_info)
在Class文件、字段表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的信息。
下文只介紹了其中一些屬性:
類型 | 名稱 | 數量 | 描述 |
u2 | attribute_name_index | 1 | 常量值固定為Code,代表該屬性名稱 |
u4 | attribute_length | 1 | 屬性值長度 |
u2 | max_stack | 1 | 操作數棧深度的最大值 |
u2 | max_locals | 1 | 局部變量表所需的存儲空間 |
u4 | code_length | 1 | 字節碼長度 |
u1 | code | code_length | 存儲字節碼指令的一系列字節流 |
u2 | exception_table_length | 1 | 異常處理表長度 |
exception_info | exception_table | exception_table_length | 異常屬性表 |
u2 | attributes_count | 1 | 屬性集合中屬性個數 |
attribute_info | attributes | attributes_count | 屬性信息 |
其中max_locals代表局部變量的存儲空間,單位為Slot(虛擬機為局部變量分配內存所使用的最小單位)。對於byte、char、float、int、short等長度不超過32位的數據類型,每個局部變量占用1個Slot,而double和long這兩種64位的數據類型則需要兩個Slot來存放。方法參數,包括實例方法中的隱藏參數“this”、顯式異常處理器的參數、方法體中定義的局部變量都需要使用局部變量表來存放。max_locals並不是簡單將所有局部變量所占Slot之和作為其值,java編譯器會根據變量的作用域來分配Slot給各個變量使用,然后計算max_locals的大小。
字節碼中每個u1類型的單字節代表一個指令。意義請自行查找虛擬機字節碼指令表。
類型 | 名稱 | 數量 | 類型 | 名稱 | 數量 |
u2 | start_pc | 1 | u2 | handler_pc | 1 |
u2 | end_pc | 1 | u2 | catch_type | 1 |
這些字段的含義為:如果當字節碼在第start_pc行(相對於方法體開始的偏移量)到第end_pc行(不包括)之間出現類型為catch_type或其子類的異常,則轉到handler_pc行繼續處理。當catch_type為0時,代表任意異常情況都需要轉向handler_pc行處處理。
類型 | 名稱 | 數量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
該屬性用於描述java源碼行號與字節碼行號(偏移量)之間的對應關系,line_number_table是一個數量為line_number_table_length、類型為line_number_info的集合,line_number_info表包括start_pc和line_number兩個u2類型的數據項,前者是字節碼行號,后者是java源碼行號。
后面是另一個方法的字節碼,就不再贅述。