深入理解java編譯後的位元組碼檔案
也許你寫了無數行的程式碼,也許你能非常溜的使用高階語言,但是你未必瞭解那些高階語言的執行過程。例如大行其道的Java。
Java號稱是一門“一次編譯到處執行”的語言,但是我們對這句話的理解深度又有多少呢?從我們寫的java檔案到通過編譯器編譯成java位元組碼檔案(也就是.class檔案),這個過程是java編譯過程;而我們的java虛擬機器執行的就是位元組碼檔案。不論該位元組碼檔案來自何方,由哪種編譯器編譯,甚至是手寫位元組碼檔案,只要符合java虛擬機器的規範,那麼它就能夠執行該位元組碼檔案。那麼本文主要講講java位元組碼檔案相關知識。接下來我們通過具體的Demo來深入理解:
1 首先我們來寫一個java原始檔
javasrc.png
上面是我們寫的一個java程式,很簡單,只有一個成員變數a以及一個方法testMethod() 。
2 接下來我們用javac命令或者ide工具將該java原始檔編譯成java位元組碼檔案。
demo.png
上圖是編譯好的位元組碼檔案,我們可以看到一堆16進位制的位元組。如果你使用IDE去開啟,也許看到的是已經被反編譯的我們所熟悉的java程式碼,而這才是純正的位元組碼,這也是我們今天需要講的內容重點。
也許你會對這樣一堆位元組碼感到頭疼,不過沒關係,我們慢慢試著你看懂它,或許有不一樣的收穫。在開始之前我們先來看一張圖
java_byte.jpeg
這張圖是一張java位元組碼的總覽圖,我們也就是按照上面的順序來對位元組碼進行解讀的。一共含有10部分,包含魔數,版本號,常量池等等,接下來我們按照順序一步一步解讀。
3.1 魔數
從上面的總覽圖中我們知道前4個位元組表示的是魔數,對應我們Demo的是 0XCAFE BABE。什麼是魔數?魔數是用來區分檔案型別的一種標誌,一般都是用檔案的前幾個位元組來表示。比如0XCAFE BABE表示的是class檔案,那麼有人會問,檔案型別可以通過檔名字尾來判斷啊?是的,但是檔名是可以修改的(包括字尾),那麼為了保證檔案的安全性,將檔案型別寫在檔案內部來保證不被篡改。
從java的位元組碼檔案型別我們看到,CAFE BABE翻譯過來是咖啡寶貝之意,然後再看看java圖示。
java_icon.png
CAFE BABE = 咖啡。
3.2 版本號
我們識別了檔案型別之後,接下來要知道版本號。版本號含主版本號和次版本號,都是各佔2個位元組。在此Demo種為0X0000 0033。其中前面的0000是次版本號,後面的0033是主版本號。通過進位制轉換得到的是次版本號為0,主版本號為51。
從oracle官方網站我們能夠知道,51對應的正式jdk1.7,而其次版本為0,所以該檔案的版本為1.7.0。如果需要驗證,可以在用java –version命令輸出版本號,或者修改編譯目標版本–target重新編譯,檢視編譯後的位元組碼檔案版本號是否做了相應的修改。
至此,我們共瞭解了前8位元組的含義,下面講講常量池相關內容。
3.3 常量池
緊接著主版本號之後的就是常量池入口。常量池是Class檔案中的資源倉庫,在接下來的內容中我們會發現很多地方會涉及,如Class Name,Interfaces等。常量池中主要儲存2大類常量:字面量和符號引用。字面量如文字字串,java中宣告為final的常量值等等,而符號引用如類和介面的全侷限定名,欄位的名稱和描述符,方法的名稱和描述符。
為什麼需要類和介面的全侷限定名呢?系統引用類或者介面的時候不是通過記憶體地址進行操作嗎?這裡大家仔細想想,java虛擬機器在沒有將類載入到記憶體的時候根本都沒有分配記憶體地址,也就不存在對記憶體的操作,所以java虛擬機器首先需要將類載入到虛擬機器中,那麼這個過程設計對類的定位(需要載入A包下的B類,不能載入到別的包下面的別的類中),所以需要通過全侷限定名來判別唯一性。這就是為什麼叫做全域性,限定的意思,也就是唯一性。
在進行具體常量池分析之前,我們先來了解一下常量池的專案型別表:
jvm_constant.png
上面的表中描述了11中資料型別的結構,其實在jdk1.7之後又增加了3種(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。這樣算起來一共是14種。接下來我們按照Demo的位元組碼進行逐一翻譯。
0×0015:由於常量池的數量不固定(n+2),所以需要在常量池的入口處放置一項u2型別的資料代表常量池數量。因此該16進位制是21,表示有20項常量,索引範圍為1~20。明明是21,為何是20呢?因為Class檔案格式規定,設計者就講第0項保留出來了,以備後患。從這裡我們知道接下來我們需要翻譯出20項常量。
Constant #1 (一共有20個常量,這是第一個,以此類推…)
0x0a-:從常量型別表中我們發現,第一個資料均是u1型別的tag,16進位制的0a是十進位制的10,對應表中的MethodRef_info。
0x-00 04-:Class_info索引項#4
0x-00 11-:NameAndType索引項#17
Constant #2
0x-09: FieldRef_info
0×0003 :Class_info索引項#3
0×0012:NameAndType索引項#18
Constant #3
0×07-: Class_info
0x-00 13-: 全侷限定名常量索引為#19
Constant #4
0x-07 :Class_info
0×0014:全侷限定名常量索引為#20
Constant #5
0×01:Utf-8_info
0x-00 01-:字串長度為1(選擇接下來的一個位元組長度轉義)
0x-61:”a”(十六進位制轉ASCII字元)
Constant #6
0×01:Utf-8_info
0x-00 01:字串長度為1
0x-49:”I”
Constant #7
0×01:Utf-8_info
0x-00 06:字串長度為6
0x-3c 696e 6974 3e-:”<init>”
Constant #8
0×01 :UTF-8_info
0×0003:字串長度為3
0×2829 56:”()V”
Constant #9
0x-01:Utf-8_info
0×0004:字串長度為4
0x436f 6465:”Code”
Constant #10
0×01:Utf-8_info
0×00 0f:字串長度為15
0x4c 696e 654e 756d 6265 7254 6162 6c65:”LineNumberTable”
Constant #11
ox01: Utf-8_info
0×00 12字串長度為18
0x-4c 6f63 616c 5661 7269 6162 6c65 5461 626c 65:”LocalVariableTable”
Constant #12
0×01:Utf-8_info
0×0004 字串長度為4
0×7468 6973 :”this”
Constant #13
0×01:Utf-8_info
0x0f:字串長度為15
0x4c 636f 6d2f 6465 6d6f 2f44 656d 6f3b:”Lcom/demo/Demo;”
Constant #14
0×01:Utf-8_info
0×00 0a:字串長度為10
ox74 6573 744d 6574 686f 64:”testMethod”
Constant #15
0×01:Utf-8_info
0x000a:字串長度為10
0x536f 7572 6365 4669 6c65 :”SourceFile”
Constant #16
0×01:Utf-8_info
0×0009:字串長度為9
0x-44 656d 6f2e 6a61 7661 :”Demo.java”
Constant #17
0x0c :NameAndType_info
0×0007:欄位或者名字名稱常量項索引#7
0×0008:欄位或者方法描述符常量索引#8
Constant #18
0x0c:NameAndType_info
0×0005:欄位或者名字名稱常量項索引#5
0×0006:欄位或者方法描述符常量索引#6
Constant #19
0×01:Utf-8_info
0×00 0d:字串長度為13
0×63 6f6d 2f64 656d 6f2f 4465 6d6f:”com/demo/Demo”
Constant #20
0×01:Utf-8_info
0×00 10 :字串長度為16
0x6a 6176 612f 6c61 6e67 2f4f 626a 6563 74 :”java/lang/Object”
到這裡為止我們解析了所有的常量。接下來是解析訪問標誌位。
3.4 Access_Flag 訪問標誌
訪問標誌資訊包括該Class檔案是類還是介面,是否被定義成public,是否是abstract,如果是類,是否被宣告成final。通過上面的原始碼,我們知道該檔案是類並且是public。
access_flag.png
0x 00 21:是0×0020和0×0001的並集。其中0×0020這個標誌值涉及到了位元組碼指令,後期會有專題對位元組碼指令進行講解。期待中……
3.5 類索引
類索引用於確定類的全限定名
0×00 03 表示引用第3個常量,同時第3個常量引用第19個常量,查詢得”com/demo/Demo”。#3.#19
3.6父類索引
0×00 04 同理:#4.#20(java/lang/Object)
3.7 介面索引
通過java_byte.jpeg圖我們知道,這個介面有2+n個位元組,前兩個位元組表示的是介面數量,後面跟著就是介面的表。我們這個類沒有任何介面,所以應該是0000。果不其然,查詢位元組碼檔案得到的就是0000。
3.8 欄位表集合
欄位表用於描述類和介面中宣告的變數。這裡的欄位包含了類級別變數以及例項變數,但是不包括方法內部宣告的區域性變數。
同樣,接下來就是2+n個欄位屬性。我們只有一個屬性a,按道理應該是0001。查詢檔案果不其然是0001。
那麼接下來我們要針對這樣的欄位進行解析。附上欄位表結構圖
欄位表結構.png
0×00 02 :訪問標誌為private(自行搜尋欄位訪問標誌)
0×00 05 : 欄位名稱索引為#5,對應的是”a”
0x 00 06 :描述符索引為#6,對應的是”I”
0x 00 00 :屬性表數量為0,因此沒有屬性表。
tips:一些不太重要的表(欄位,方法訪問標誌表)可以自行搜尋,這裡就不貼出來了,防止篇幅過大。
3.9 方法
我們只有一個方法testMethod,按照道理應該前2個位元組是0001。通過查詢發現是0×00 02。這是什麼原因,這代表著有2個方法呢?且繼續看……
方法表結構.png
上圖是一張方法表結構圖,按照這個圖我們分析下面的位元組碼:
第1個方法:
0×00 01:訪問標誌 ACC_PUBLIC,表明該方法是public。(可自行搜尋方法訪問標誌表)
0×00 07:方法名索引為#7,對應的是”<init>”
0×00 08:方法描述符索引為#8,對應的是”()V”
0×00 01:屬性表數量為1(一個屬性表)
那麼這裡涉及到了屬性表。什麼是屬性表呢?可以這麼理解,它是為了描述一些專有資訊的,上面的方法帶有一張屬性表。所有屬性表的結構如下圖:
一個u2的屬性名稱索引,一個u2的屬性長度加上屬性長度的info。
虛擬機器規範預定義的屬性有很多,比如Code,LineNumberTable,LocalVariableTable,SourceFile等等,這個網上可以搜尋到。
屬性表結構.png
按照上面的表結構解析得到下面資訊:
0×0009:名稱索引為#9(“Code”)。
0×000 00038:屬性長度為56位元組。
那麼接下來解析一個Code屬性表,按照下圖解析
code.png
前面6個位元組(名稱索引2位元組+屬性長度4位元組)已經解析過了,所以接下來就是解析剩下的56-6=50位元組即可。
0×00 02 :max_stack=2
0×00 01 : max_locals=1
0×00 0000 0a : code_length=10
0x2a b700 012a 04b5 0002 b1 : 這是code程式碼,可以通過虛擬機器位元組碼指令進行查詢。
2a=aload_0(將第一個引用變數推送到棧頂)
b7=invokespecial(呼叫父類構造方法)
00=什麼都不做
01 =將null推送到棧頂
2a=同上
04=iconst_1 將int型1推送到棧頂
b5=putfield 為指定的類的例項變數賦值
00= 同上
02=iconst_m1 將int型-1推送棧頂
b1=return 從當前方法返回void
整理,去除無動作指令得到下面
0 : aload_0
1 : invokespecial
4 : aload_0
5 : iconst_1
6 : putfield
9 : return
關於虛擬機器位元組碼指令這塊內容,後期會繼續深入下去…… 目前只需要瞭解即可。接下來順著Code屬性表繼續解析下去:
0×00 00 : exception_table_length=0
0×00 02 : attributes_count=2(Code屬性表內部還含有2個屬性表)
0×00 0a: 第一個屬性表是”LineNumberTable”
LineNumberTable.png
0×00 0000 0a : “屬性長度為10″
0×00 02 :line_number_table_length=2
line_number_table是一個數量為line_number_table_length,型別為line_number_info的集合,line_number_info表包括了start_pc和line_number兩個u2型別的資料項,前者是位元組碼行號,後者是Java原始碼行號
0×00 00 : start_pc =0
0×00 03 : end_pc =3
0×00 04 : start_pc=4
0×00 04 : end_pc=4
0×00 0b 第二個屬性表是:”LocalVariableTable”
local_variable_table.png
local_variable_info.png
0×00 0000 0c:屬性長度為12
0×00 01 : local_variable_table_length=1
然後按照local_variable_info表結構進行解析:
0×00 00 : start_pc=0
0×00 0a:length=10
0x000c : name_index=”this”
0x000d : descriptor_index #13 (“Lcom/demo/Demo”)
0000 index=0
//——-到這裡第一個方法就解析完成了——-//
Method(<init>)–1個屬性Code表-2個屬性表(LineNumberTable ,LocalVariableTable)接下來解析第二個方法
第2個方法:
0×00 04:”protected”
0×00 0e: #14(”testMethod”)
0×00 08 : “()V”
0×0001 : 屬性數量=1
0×0009 :”Code”
0×0000 002b 屬性長度為43
解析一個Code表
0000 :max_stack =0
0001 : max_local =1
0000 0001 : code_length =1
0xb1 : return(該方法返回void)
0×0000 異常表長度=0
0×0002 屬性表長度為2
//第一個屬性表
0x000a : #10,LineNumberTable
0×0000 0006 : 屬性長度為6
0×0001 : line_number_length = 1
0×0000 : start_pc =0
0×0008 : end_pc =8
//第二個屬性表
0x000b : #11 ,LocalVariableTable
0×0000 000c : 屬性長度為12
0×0001 : local_variable_table_length =1
0×0000 :start_pc = 0
0×0001: length = 1
0x000c : name_index =#12 “this”
0x000d : 描述索引#13 “Lcom/demo/Demo;”
0000 index=0
//到這裡為止,方法解析都完成了,回過頭看看頂部解析順序圖,我們接下來就要解析Attributes了。
3.10 Attribute
0×0001 :同樣的,表示有1個Attributes了。
0x000f : #15(“SourceFile”)
0×0000 0002 attribute_length=2
0×0010 : sourcefile_index = #16(“Demo.java”)
SourceFile屬性用來記錄生成該Class檔案的原始碼檔名稱。
source_file.jpeg
4 另話
其實,我們寫了這麼多確實很麻煩,不過這種過程自己體驗一遍的所獲所得還是不同的。現在,使用java自帶的反編譯器來解析位元組碼檔案。
javap -verbose Demo //不用帶字尾.class
javap_result.png
5 總結
到此為止,講解完成了class檔案的解析,這樣以後我們也能看懂位元組碼檔案了。瞭解class檔案的結構對後面進一步瞭解虛擬機器執行引擎非常重要,所以這是基礎並重要的一步。