1. 程式人生 > >一文讓你明白Java字節碼

一文讓你明白Java字節碼

簡單 文件的 asc IE 自帶 搜索 頂部 ans 系統

也許你寫了無數行的代碼,也許你能非常溜的使用高級語言,但是你未必了解那些高級語言的執行過程。例如大行其道的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文件的結構對後面進一步了解虛擬機執行引擎非常重要,所以這是基礎並重要的一步。

一文讓你明白Java字節碼