00 前言
JAVA程序具有 " Write Once , Run Anywhere ." 的跨平臺(tái)特性。實(shí)現(xiàn)這樣的目的,Java的方案是:半編譯 + 半解釋,即 .Class + JVM 。

1、源程序內(nèi)容會(huì)被編譯為.Class文件,.Class文件具有嚴(yán)格規(guī)定如何從中提取信息,可以理解為 “中間碼”,約定使用者如何理解文件內(nèi)容
2、理解了程序內(nèi)容,各個(gè)平臺(tái)根據(jù)自身特色不同,實(shí)現(xiàn)各自的JVM用來解釋(翻譯).Class文件,變成真正的本地可執(zhí)行指令。
如此實(shí)現(xiàn)了Java跨平臺(tái)的特性。因此,跨平臺(tái)的基礎(chǔ)為.Class,實(shí)現(xiàn)為JVM。
本文的目的為:讀懂.Class,悉知編寫的程序代碼在JVM眼中是什么樣子。而在理解了.Class之后,對于理解JVM、理解字節(jié)碼插樁等有進(jìn)一步幫助。
01 基礎(chǔ)知識(shí)
字節(jié)碼
字節(jié)碼是一種包含執(zhí)行程序,由數(shù)據(jù)對組成的二進(jìn)制文件,是一種中間碼。一般來說,一字節(jié)占用8位,即包含八位的二進(jìn)制。
文章所指的.Class文件為字節(jié)碼文件,每字節(jié)用16進(jìn)制表示,數(shù)值范圍 00 ~ FF (0 ~ 255).
無符號(hào)數(shù)基本類型
無符號(hào)數(shù)可以用來描述數(shù)字、索引引用、數(shù)值量或按照 UTF-8 編碼構(gòu)成字符串。u1、u2、u4、u8分別代表 1個(gè)字節(jié)、2個(gè)字節(jié)、4個(gè)字節(jié)和8個(gè)字節(jié)的無符號(hào)數(shù)。
字面量
字面量是一種固定值的表示法,本身沒有含義,需要場景來為它賦予含義,如何理解?比如 007 沒有含義,但是用來表示詹姆斯·邦德,你就知道007代表一個(gè)很厲害的特工。在程序中,int x = 10、String s = "10" 讓字面量 10 具有了不同的意義。
全限定名
將一個(gè)類的全限定名是將類全名的.全部替換為/,如java.lang.String替換為java/lang/String
描述符
描述符用來描述字段的數(shù)據(jù)類型、方法的參數(shù)類表和返回值,每種符號(hào)對應(yīng)不同數(shù)據(jù)類型

02 Class文件
Java文件包含了一個(gè)類的所有信息,以下是一個(gè)Java類:
import java.io.Serializable;
public class TestClass implements Serializable{
private int m = 123;
private static int x = 10;
private static final int y = 20;
public int increace(){
return m+1;
}
public void m() throws Exception{
// 具體邏輯不寫
}
public static String hello(){
return "hello word";
}
}
此Java文件中,所包含的信息有:
類明為TestClass并可被外部訪問,實(shí)現(xiàn)了Serializable接口
擁有類變量 x和y,擁有成員變量 m
擁有可被外部訪問的類函數(shù) hello(),擁有可被外部訪問的成員函數(shù)increace() 和 m()
note: 如無特殊說明,文章所說.Class文件均由此Java文件編譯得來這些信息在被編譯后將在.Class文件中進(jìn)行表達(dá)。通過命令
Javac fileName.java
可將Java文件編譯成對應(yīng)的.Class文件。.Class文件為字節(jié)碼文件,可借助對應(yīng)編輯器閱讀。
本文使用的編輯器為 “010” ,windows 和 mac 都有, 自行下載。

Class 字節(jié)碼實(shí)例文件
.Class文件使用字節(jié)碼表達(dá)信息,各數(shù)據(jù)間緊湊,不包含任何分隔符,因此整個(gè).Class文件中存儲(chǔ)的內(nèi)容幾乎是全部程序運(yùn)行時(shí)的必要數(shù)據(jù)。如何解析字節(jié)碼數(shù)據(jù),就需要制定規(guī)則來解讀,嚴(yán)格遵守。
.Class 文件風(fēng)格采用類似于C語言結(jié)構(gòu)的偽結(jié)構(gòu)來存儲(chǔ)數(shù)據(jù)。可以將.Class文件看成多張表的集合,通過表索引,能找到對應(yīng)的數(shù)據(jù)。可以理解為,數(shù)據(jù)存在的相對位置,決定了它被賦予的涵義。
.Class文件格式如下表

有些數(shù)據(jù)信息是定長的,有些視具體情況而定,但都會(huì)有相應(yīng)的約束告知具體長度。各信息對應(yīng)已在.Class實(shí)例文件圖標(biāo)出,剩下的是逐層去解析類信息。
03 常量池
常量池中主要存放兩大類:字面量和符號(hào)引用。符號(hào)引用包括:
類和接口的全限定名
字段的名稱和描述符
方法的名稱和描述符
與C和C++不同,Java代碼編譯后沒有“連接”的步驟,在JVM加載Class文件的時(shí)候進(jìn)行動(dòng)態(tài)連接。.Class文件不會(huì)保存各方法、字段的最終內(nèi)存布局信息,因?yàn)椴荒芙?jīng)過運(yùn)行期轉(zhuǎn)換無法得到真正的內(nèi)存入庫地址,無法被JVM使用。在JVM運(yùn)行時(shí),從常量池中拿到對應(yīng)的符號(hào)引用,解析、翻譯到具體的內(nèi)存地址中再進(jìn)行使用,這些信息也就存于JVM的方法區(qū)中。

常量池所占用長度不定,需要 0x0008 ~ 0x0009 提供常量數(shù)量統(tǒng)計(jì),再根據(jù)常量池里的具體常量類型推算出具體總占用的長度。
但這比較繁瑣,每一種常量類型對應(yīng)一份表,需要根據(jù)表的不同查閱具體的表結(jié)構(gòu)來獲取信息。常量類型的第一位 u1 表明來對應(yīng)常量的表結(jié)構(gòu),對應(yīng)信息如下

除必要外,本文不打算列出各個(gè)對應(yīng)的表結(jié)構(gòu),具體結(jié)構(gòu)可參考
Class類文件結(jié)構(gòu)之常量表
https://blog.csdn.net/u014296316/article/details/83020087
這里拋磚引玉。

常量池第一個(gè)常量
第一個(gè)常量類型由 0x000A 處標(biāo)出,值為0A,十進(jìn)制為10,查表,類型為CONSTANT_Methodref_info(不得不說現(xiàn)在的編輯器很強(qiáng)大,沒有對應(yīng)功能的話就只能慢慢查了)。
表中數(shù)據(jù)類型為u1、u2、u2 共占 5 個(gè)字節(jié)(具體表信息和內(nèi)容含義后文再續(xù))。如果是CONSTANT_Utf8_info類型,還會(huì)有 length 屬性表明字面量占用字節(jié)長度,需要加上此長度。則第二個(gè)常量類型由 0x000F 處標(biāo)出,值為0F,十進(jìn)制為09,表類型為CONSTANT_Fieldref_info。以此類推...
這樣一步一步查找對應(yīng)常量也是比較麻煩的,好在Java內(nèi)置類工具——javap可對.Class文件字節(jié)碼進(jìn)行分析,通過命令
javap -verbose fileName
能得到下圖信息 :(僅展示了常量池部分)

javap分析常量池
常量池?cái)?shù)量值在 0x0008 ~ 0x0009 為 23,轉(zhuǎn)換十進(jìn)制為35,表示常量池索引范圍為 1~35。觀察上兩張圖,前者索引從0開始,后者索引從1開始。
若摸不著門路,常量池的分析著實(shí)讓人頭大,個(gè)人看來,常量池里的信息是在 “搭積木” 。
本例子中,常量池涉及到的常量類型為:
CONSTANT_Methodref_info
CONSTANT_Fieldref_info
CONSTANT_String_info
CONSTANT_Class_info
CONSTANT_Interger
CONSTANT_NameAndType
CONSTANT_Utf8
暫時(shí)拋開具體表結(jié)構(gòu),以上表類型結(jié)構(gòu)關(guān)系如示:

常量池常量類型結(jié)構(gòu)
上面僅畫出了當(dāng)前例子涉及到的常量類型的組成關(guān)系,任意類型的常量,不斷拆分,最后都會(huì)指向基本類型的常量CONSTANT_Utf8,或自身就為基本類型如CONSTANT_Interger。
可以理解為,基本常量類型CONSTANT_Utf8本身沒有過多意義,其它的類型為場景,為CONSTANT_Utf8賦予了意義。
CONSTANT_Utf8_info可以算是最基本的類型,結(jié)構(gòu)為
// 偽代碼
{
// 常量類型
u1 tag;
// 字節(jié)長度
u2 length;
// UTF-8縮略編碼
bytes[length];
}
在遇到CONSTANT_Utf8_info類型的常量時(shí),將bytes逐個(gè)按照UTF-8縮略編碼即可得到對應(yīng)的字面量
04 類級信息
定義的類為
public class TestClass implements Serializable
其中包含的信息為:
類本身:TestClass
訪問標(biāo)志:public
實(shí)現(xiàn)接口Serializable
父類為:Object
從.Class文件格式表中,在常量池后緊接著的數(shù)據(jù),就是類級數(shù)據(jù)

從 0x0143 ~ 0x014C :
access_flags(u2): 十六進(jìn)制值為 0x0021
this_class(u2): 十進(jìn)制值為5,指向常量池第5個(gè)常量, 類型為CONSTANT_Class_info,類為 TestClass
super_class(u2): 十進(jìn)制值為5,指向第6個(gè)常量,類型為CONSTANT_Class_info,類為 java/lang/Object
interface_count(u2): 實(shí)現(xiàn)接口數(shù)量 1 個(gè)
interface[0] :指向常量池第7個(gè)常量,類型為CONSTANT_Class_info,接口名誒 java/io/Serializable
CONSTANT_Class_info 常量表結(jié)構(gòu)如下// 偽代碼
{
// 常量類型
u1 tag ;
// 指向常量池偏移量為name_index,類型為CONSTANT_Utf8_info類型的索引,
//代表類或接口的權(quán)限定名
u2 name_index;
}
與之前所說常量池里在搭積木的說法一致,后面涉及到的常量池里的類型依然如此。
訪問標(biāo)志使用標(biāo)志位來表示,各個(gè)標(biāo)志含義如表

當(dāng)前情況為 0x0001 | 0x0020 = 0x0021
05 attribute(屬性表)
屬性表比較特殊,.Class文件、字段表、方法表等都可以攜帶自己的屬性表集合,用來描述專有的場景,也因此將此表做前置說明。
屬性表的特點(diǎn)為:
規(guī)則較寬松,不要求嚴(yán)格的順序、長度、內(nèi)容
只要不與已有屬性表重復(fù),任何編譯器都可以向?qū)傩员碇袑懭胱远x的屬性信息,JVM會(huì)忽略掉不認(rèn)識(shí)的屬性。
屬性表結(jié)構(gòu)為// 偽代碼
{
// 指向常量池類型為CONSTANT_Utf8_info的常量,代表屬性名
u2 attribute_name_index ;
// 屬性表info占用長度
u4 attribute_length;
// 這需要具體實(shí)現(xiàn)的結(jié)構(gòu),長度為attribute_length
Info info;
}
因此一個(gè)屬性表的長度為 u2 + u4 + attribute_length。Java與定義來很多屬性表,文章檢出涉及到的做后續(xù)說明,其它在實(shí)際需要時(shí)自行查閱

字段表與方法表攜帶的屬性表暫未涉及,當(dāng)前節(jié)點(diǎn)涉及到類型為SourceFile的.Class攜帶的屬性表。

class攜帶屬性表示例
范圍為 0x025A ~ 0x0262 共占 u2 + u4 + attibute_length = 8 字節(jié),SourceFile屬性結(jié)構(gòu)如下
偽代碼
{
// 指向常量池類型為CONSTANT_Utf8_info的常量,代表屬性名
u2 attribute_name_index ;
// 屬性表內(nèi)容占用長度
u4 attribute_length;
// 指向常量池類型為CONSTANT_Utf8_info的常量,代表源文件名
u2 sourcefile_index;
}
因此通過此SourceFile屬性表,得知源文件名為TestClass.java
06 字段表
查閱.Class文件格式表,接口表之后,就是字段數(shù)量已經(jīng)字段數(shù)量表

字節(jié)碼字段表
從 0x014D ~ 0x016E , 其中 0x014D ~ 0x014E 表示字段數(shù), 值為 0x0003,表示字段數(shù)為3,隨便就是緊挨著的字段表。字段表結(jié)構(gòu)如下
// 偽代碼
{
// 訪問標(biāo)志
u2 access_flags
// 指向常量池類型為CONSTANT_Utf8_info的常量,表示字段名
u2 name_index
// 指向常量池類型為CONSTANT_Utf8_info的常量,用描述符表示
// 字段類型
u2 descriptor_index
// 屬性表數(shù)量
attributes_count
// 屬性表內(nèi)容
attribuite_info
}
字段表和.Class一樣,能攜帶自己的屬性表處理特殊場景,attribuite_info是非必須的。當(dāng)attributes_count的值為0時(shí), 說明無需attribuite_info。字段也擁有訪問標(biāo)志來對字段做進(jìn)一步約束。字段名則用name_index指向常量池的常量來表示,字段類型則用描述符來表示, 比如 Int 表示為 I (忘了往前看基礎(chǔ)知識(shí))。
當(dāng)前例子定義的字段如下:
private int m = 123;
private static int x = 10;
private static final int y = 20;
定義了成員變量 m 和 類變量 x ,y。舉例 y 來看

位置為 0x015F ~ 0x016E,其中:
訪問標(biāo)志:0x001A
字段名索引:0x000B,十進(jìn)制值為11,指向常量池第11個(gè)常量,為y
描述符索引為:0x0009,指向常量池第9個(gè)常量,為I
屬性表數(shù)為:0x0001,數(shù)量為1
屬性表總占用長度為:u2 + u4 + 2字節(jié) 共10位,即 0x0167 ~ 0x016E
字段訪問標(biāo)志位含義如表:

當(dāng)前為 private static final ,即 0x0001 | 0x0008 | 0x0010 , 為 0x001A。
通過訪問標(biāo)志、字段索引、描述符信息,就可以拿到
-> private static final int y 這一信息。
對于使用 final和static修飾的并且時(shí)基本數(shù)據(jù)類型的變量,會(huì)使用屬性表ConstantVulue來進(jìn)行賦值。屬性表 ConstantVulue 除了約定的基本數(shù)據(jù)外,還有類型為 u2 的ConstantValue_index 索引來表示指向常量池中的常量用來初始化數(shù)據(jù)。值位于 0x016D ~ 0x016E,為 0x000D,指向的常量表索引13處的值為整型的20;
而實(shí)例中的變量m,類變量x則在成員初始函數(shù)、類初始函數(shù)中進(jìn)行賦值,下文做說明。
07 方法表
字段表之后,緊挨著的是方法數(shù)量與方法表集合

范圍為 0x016F ~ 0x0258,方法數(shù)值為 0x0005 共 5 個(gè)方法。除了示例自定義的 increace() ,m() 和 hello() 外,還有實(shí)例構(gòu)造方法<init>()v,類構(gòu)造器<clinit>()方法。
方法表結(jié)構(gòu)為:
偽代碼
{
// 訪問標(biāo)志
u2 access_flags;
// 方法名索引,指向常量池類型為CONSTANT_Utf8_info的常量
u2 name_index;
// 方法返回值描述符索引,指向常量池類型為CONSTANT_Utf8_info的常量
u2 descriptor_index;
// 屬性表數(shù)量
u2 attributes_count;
// 屬性表內(nèi)容
Info info;
}
方法也可以攜帶屬性表來描述專有場景。通過上述結(jié)構(gòu)以及具體字節(jié)碼,可以推算出方法所包含的內(nèi)容。其中,訪問標(biāo)志含義如表:

取構(gòu)造實(shí)例方法方法<init>()做示例來看:

信息為:
access_flags: 0x0001 ,為public
name_index: 0x000E,為14,對應(yīng)常量池得到 <init>
descriptor_index: 0x000F,為15,對應(yīng)常量池得到 ()V
attributes_count: 0x0001,為1,屬性表數(shù)量為1
可以根據(jù)信息反推得到函數(shù)信息,實(shí)例化函數(shù)被表達(dá)為 public <init>()V,依次為訪問標(biāo)志位、函數(shù)名、返回值。
一個(gè)函數(shù)方法,更重要的如何表述它所提供的功能。本質(zhì)上來說,函數(shù)體里所有的代碼段都是在進(jìn)行運(yùn)算操作,因此,只要將函數(shù)體里的代碼段轉(zhuǎn)換為字節(jié)碼指令即可,函數(shù)執(zhí)行時(shí)根據(jù)執(zhí)行即可,再次之上再幾率一下關(guān)鍵信息,就能得出函數(shù)執(zhí)行、棧深度、局部變量數(shù)、字節(jié)碼占用文件大小。
在attributes_count之后,從 0x0179 ~ 0x01A5 是屬性表包含的內(nèi)容。根據(jù)之前所說的屬性表約定的格式 0x0179 ~ 0x0180 位置值為 0x0010,十進(jìn)制為16,查找啊常量池知屬性表為Code類型。Code類型屬性表結(jié)構(gòu)如下:
// 偽代碼
{
// 屬性表名稱索引,指向常量表類型為CONSTANT_Utf8_info的常量
u2 attribute_name_index;
// 屬性表長度
u4 attribute_length;
// 棧深
u2 max_stack;
// 局部變量數(shù)
u2 max_locals;
// 字節(jié)碼指令長度
u4 code_length;
// 字節(jié)碼指令
u1 code code_length;
// 異常表數(shù)量
u2 exception_table_length;
// 異常表
exception_info exception_table;
// 屬性表數(shù)量
u2 attributes_count;
// 屬性表
attribute_info attributes;
}
獲取方法信息不僅能直接通過閱讀字節(jié)碼文件,通過javap -verbose className也可以拿到,一起貼了

紅圈為字節(jié)碼指令集,黃圈為每一條字節(jié)碼指令,綠圈為Code屬性表的基本信息,藍(lán)圈為javap工具解析出的實(shí)例函數(shù)信息。
首先,最大棧深為2,在函數(shù)執(zhí)行的任意時(shí)刻都不會(huì)超過這個(gè)操作數(shù)棧深度的最大值;然后局部變量數(shù)為1,表示局部變量表所需的存儲(chǔ)空間,單位為Slot,此單位是JVM為局部變量分配內(nèi)存所使用的最小單位。藍(lán)圈里還有args_sige表示方法接受的參數(shù)數(shù),這里為1,也就是 this。最后就是函數(shù)代碼塊里轉(zhuǎn)成的字節(jié)碼指令里。
字節(jié)碼指令不在文章的討論范圍內(nèi),不妨簡單了解。
字節(jié)碼指令代表著某種特定操作,由一個(gè)字節(jié)長度代表其操作含義,后面可以跟隨0到多個(gè)所需操作數(shù)。
本例子中的初始化函數(shù)被翻譯成了:
2A B7 00 01 2A 10 7B B5 00 02 B1
也就是上圖藍(lán)圈處的:
{
// 將 this 入棧
0: aload_0
// 喚醒父類實(shí)例化函數(shù)
1: invokespecial #1
4: aload_0
// 將 123 入棧
5: bitpush 123
// 訪問字段 m,將123存入
7: putfield #2
// 方法返回
10: return
}
這里只說明 putfield #2,對應(yīng)的字節(jié)碼為 B5 00 02,其中 B5 代表操作執(zhí)行 00 02 為操作所需參數(shù),值為 0x0002,表示指向常量池類型為 CONSTANT_Fieldref_info 的常量。CONSTANT_Fieldref_info 結(jié)構(gòu)為
// 偽代碼
CONSTANT_Fieldref_info
{
u1 tag;
// 指向常量池類型為CONSTANT_ClassInfo_info的常量
// 代表字段所屬類
u2 class_index;
// 指向常量池類型為CONSTANT_NameAndType_info的常量
// 代表字段名和類型
u2 name_and_type_index;
}
CONSTANT_ClassInfo_info
{
u1 tag;
// 指向常量池尾CONSTANT_Utf8_info的常量
// 代表類名
u2 name_index;
}
CONSTANT_NameAndType_info
{
u1 tag;
// 指向常量池尾CONSTANT_Utf8_info的常量
// 代表 名稱
u2 name_index;
// 指向常量池尾CONSTANT_Utf8_info的常量
// 代表 所屬類型
u2 descriptor_index;
}
當(dāng)前為指向第二個(gè)常量,對照信息:

m初始化
能知道字節(jié)碼操作 B5 00 02 是將棧中的 123 給 TestClass.m 進(jìn)行賦值 ,也就是例子中定義的 private int m = 123 的賦值操作。
而 x 的賦值則在類構(gòu)造器<cinit>中進(jìn)行,inicrea()和m()函數(shù)也可以用相同的方式進(jìn)行分析。不再陳述,點(diǎn)到為止。
08 總結(jié)
至此,了解.Class文件的如何,通過.Class文件格式表可以解析出文件內(nèi)容的基本信息。.Class文件可以看成多張表的集合,根據(jù)表制定的規(guī)則,順藤摸瓜,自然能找出對應(yīng)信息。
.Class文件在如何表達(dá)信息上不難理解,難的是有耐心去縷清這些瑣碎的索引關(guān)系,尤其常量池和屬性表部分。屬性表部分則提供了足夠的發(fā)揮空間,根據(jù)場景提供更多內(nèi)容。
文章僅了解了解析.Class文件的基本規(guī)則,更進(jìn)一步的解析規(guī)則感興趣或需要時(shí)再了解即可,方法不變。
參考
《深入理解Java虛擬機(jī)》 —— 第6章
1、深入理解JVM字節(jié)碼執(zhí)行引擎https://blog.csdn.net/suifeng629/article/details/823497842、java中class文件的意義是什么?https://blog.csdn.net/dangbai01_/article/details/800973403、java語言為什么可以跨平臺(tái)https://blog.csdn.net/banjing_1993/article/details/82349013
歡迎在留言區(qū)留下你的觀點(diǎn),一起討論提高。如果今天的文章讓你有新的啟發(fā),學(xué)習(xí)能力的提升上有新的認(rèn)識(shí),歡迎轉(zhuǎn)發(fā)分享給更多人。