一文讓你明白Java字節碼

前言

也許你寫了無數行的代碼,也許你能非常溜的使用高級語言,但是你未必瞭解那些高級語言的執行過程。例如大行其道的Java。

Java號稱是一門「一次編譯到處運行」的語言,但是我們對這句話的理解深度又有多少呢?從我們寫的java文件到通過編譯器編譯成java字節碼文件(也就是.class文件),這個過程是java編譯過程;而我們的java虛擬機執行的就是字節碼文件。不論該字節碼文件來自何方,由哪種編譯器編譯,甚至是手寫字節碼文件,只要符合java虛擬機的規範,那麼它就能夠執行該字節碼文件。那麼本文主要講講java字節碼文件相關知識。接下來我們通過具體的Demo來深入理解:

  1. 首先我們來寫一個java源文件

上面是我們寫的一個java程序,很簡單,只有一個成員變量a以及一個方法testMethod() 

2.接下來我們用javac命令或者ide工具將該java源文件編譯成java字節碼文件。

上圖是編譯好的字節碼文件,我們可以看到一堆16進制的字節。如果你使用IDE去打開,也許看到的是已經被反編譯的我們所熟悉的java代碼,而這纔是純正的字節碼,這也是我們今天需要講的內容重點。

也許你會對這樣一堆字節碼感到頭疼,不過沒關係,我們慢慢試着你看懂它,或許有不一樣的收穫。在開始之前我們先來看一張圖

這張圖是一張java字節碼的總覽圖,我們也就是按照上面的順序來對字節碼進行解讀的。一共含有10部分,包含魔數,版本號,常量池等等,接下來我們按照順序一步一步解讀。

3.1 魔數

從上面的總覽圖中我們知道前4個字節表示的是魔數,對應我們Demo的是 0XCAFE BABE。什麼是魔數?魔數是用來區分文件類型的一種標誌,一般都是用文件的前幾個字節來表示。比如0XCAFE BABE表示的是class文件,那麼有人會問,文件類型可以通過文件名後綴來判斷啊?是的,但是文件名是可以修改的(包括後綴),那麼爲了保證文件的安全性,講文件類型寫在文件內部來保證不被篡改。
從java的字節碼文件類型我們看到,CAFE BABE翻譯過來是咖啡寶貝之意,然後再看看java圖標。

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類,不能加載到別的包下面的別的類中),所以需要通過全侷限定名來判別唯一性。這就是爲什麼叫做全局,限定的意思,也就是唯一性。

在進行具體常量池分析之前,我們先來了解一下常量池的項目類型表:

上面的表中描述了11中數據類型的結構,其實在jdk1.7之後又增加了3種(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。這樣算起來一共是14種。接下來我們按照Demo的字節碼進行逐一翻譯。

0x0015:由於常量池的數量不固定(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
0x0003 :Class_info索引項#3
0x0012:NameAndType索引項#18
Constant #3
0x07-: Class_info
0x-00 13-: 全侷限定名常量索引爲#19
Constant #4
0x-07 :Class_info
0x0014:全侷限定名常量索引爲#20
Constant #5
0x01:Utf-8_info
0x-00 01-:字符串長度爲1(選擇接下來的一個字節長度轉義)
0x-61:"a"(十六進制轉ASCII字符)
Constant #6
0x01:Utf-8_info
0x-00 01:字符串長度爲1
0x-49:"I"
Constant #7
0x01:Utf-8_info
0x-00 06:字符串長度爲6
0x-3c 696e 6974 3e-:"<init>"
Constant #8
0x01 :UTF-8_info
0x0003:字符串長度爲3
0x2829 56:"()V"
Constant #9
0x-01:Utf-8_info
0x0004:字符串長度爲4
0x436f 6465:"Code"
Constant #10
0x01:Utf-8_info
0x00 0f:字符串長度爲15
0x4c 696e 654e 756d 6265 7254 6162 6c65:"LineNumberTable"
Constant #11
ox01: Utf-8_info
0x00 12字符串長度爲18
0x-4c 6f63 616c 5661 7269 6162 6c65 5461 626c 65:"LocalVariableTable"
Constant #12
0x01:Utf-8_info
0x0004 字符串長度爲4
0x7468 6973 :"this"
Constant #13
0x01:Utf-8_info
0x0f:字符串長度爲15
0x4c 636f 6d2f 6465 6d6f 2f44 656d 6f3b:"Lcom/demo/Demo;"
Constant #14
0x01:Utf-8_info
0x00 0a:字符串長度爲10
ox74 6573 744d 6574 686f 64:"testMethod"
Constant #15
0x01:Utf-8_info
0x000a:字符串長度爲10
0x536f 7572 6365 4669 6c65 :"SourceFile"
Constant #16
0x01:Utf-8_info
0x0009:字符串長度爲9
0x-44 656d 6f2e 6a61 7661 :"Demo.java"
Constant #17
0x0c :NameAndType_info
0x0007:字段或者名字名稱常量項索引#7
0x0008:字段或者方法描述符常量索引#8
Constant #18
0x0c:NameAndType_info
0x0005:字段或者名字名稱常量項索引#5
0x0006:字段或者方法描述符常量索引#6
Constant #19
0x01:Utf-8_info
0x00 0d:字符串長度爲13
0x63 6f6d 2f64 656d 6f2f 4465 6d6f:"com/demo/Demo"
Constant #20
0x01:Utf-8_info
0x00 10 :字符串長度爲16
0x6a 6176 612f 6c61 6e67 2f4f 626a 6563 74 :"java/lang/Object"
到這裏爲止我們解析了所有的常量。接下來是解析訪問標誌位。

3.4 Access_Flag 訪問標誌

訪問標誌信息包括該Class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,是否被聲明成final。通過上面的源代碼,我們知道該文件是類並且是public。

0x 00 21:是0x0020和0x0001的並集。其中0x0020這個標誌值涉及到了字節碼指令,後期會有專題對字節碼指令進行講解。期待中......

3.5 類索引

類索引用於確定類的全限定名
0x00 03 表示引用第3個常量,同時第3個常量引用第19個常量,查找得"com/demo/Demo"。#3.#19

3.6父類索引

0x00 04 同理:#4.#20(java/lang/Object)

3.7 接口索引

通過java_byte.jpeg圖我們知道,這個接口有2+n個字節,前兩個字節表示的是接口數量,後面跟着就是接口的表。我們這個類沒有任何接口,所以應該是0000。果不其然,查找字節碼文件得到的就是0000。

3.8 字段表集合

字段表用於描述類和接口中聲明的變量。這裏的字段包含了類級別變量以及實例變量,但是不包括方法內部聲明的局部變量。
同樣,接下來就是2+n個字段屬性。我們只有一個屬性a,按道理應該是0001。查找文件果不其然是0001。
那麼接下來我們要針對這樣的字段進行解析。附上字段表結構圖

0x00 02 :訪問標誌爲private(自行搜索字段訪問標誌)
0x00 05 : 字段名稱索引爲#5,對應的是"a"
0x 00 06 :描述符索引爲#6,對應的是"I"
0x 00 00 :屬性表數量爲0,因此沒有屬性表。
tips:一些不太重要的表(字段,方法訪問標誌表)可以自行搜索,這裏就不貼出來了,防止篇幅過大。

3.9 方法

我們只有一個方法testMethod,按照道理應該前2個字節是0001。通過查找發現是0x00 02。這是什麼原因,這代表着有2個方法呢?且繼續看......

上圖是一張方法表結構圖,按照這個圖我們分析下面的字節碼:

第1個方法:

0x00 01:訪問標誌 ACC_PUBLIC,表明該方法是public。(可自行搜索方法訪問標誌表)
0x00 07:方法名索引爲#7,對應的是"<init>"
0x00 08:方法描述符索引爲#8,對應的是"()V"
0x00 01:屬性表數量爲1(一個屬性表)
那麼這裏涉及到了屬性表。什麼是屬性表呢?可以這麼理解,它是爲了描述一些專有信息的,上面的方法帶有一張屬性表。所有屬性表的結構如下圖:
一個u2的屬性名稱索引,一個u2的屬性長度加上屬性長度的info。
虛擬機規範預定義的屬性有很多,比如Code,LineNumberTable,LocalVariableTable,SourceFile等等,這個網上可以搜索到。

按照上面的表結構解析得到下面信息:
0x0009:名稱索引爲#9("Code")。
0x000 00038:屬性長度爲56字節。
那麼接下來解析一個Code屬性表,按照下圖解析

前面6個字節(名稱索引2字節+屬性長度4字節)已經解析過了,所以接下來就是解析剩下的56-6=50字節即可。
0x00 02 :max_stack=2
0x00 01 : max_locals=1
0x00 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屬性表繼續解析下去:
0x00 00 : exception_table_length=0
0x00 02 : attributes_count=2(Code屬性表內部還含有2個屬性表)
0x00 0a: 第一個屬性表是"LineNumberTable"

0x00 0000 0a : "屬性長度爲10"
0x00 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源碼行號
0x00 00 : start_pc =0
0x00 03 : end_pc =3
0x00 04 : start_pc=4
0x00 04 : end_pc=4

0x00 0b 第二個屬性表是:"LocalVariableTable"

0x00 0000 0c:屬性長度爲12
0x00 01 : local_variable_table_length=1
然後按照local_variable_info表結構進行解析:
0x00 00 : start_pc=0
0x00 0a:length=10
0x000c : name_index="this"
0x000d : descriptor_index #13 ("Lcom/demo/Demo")
0000 index=0
//-------到這裏第一個方法就解析完成了-------//
Method(<init>)--1個屬性Code表-2個屬性表(LineNumberTable ,LocalVariableTable)接下來解析第二個方法

0x0001 :同樣的,表示有1個Attributes了。
0x000f : #15("SourceFile")
0x0000 0002 attribute_length=2
0x0010 : sourcefile_index = #16("Demo.java")
SourceFile屬性用來記錄生成該Class文件的源碼文件名稱。

第2個方法:

0x00 04:"protected"
0x00 0e: #14("testMethod")
0x00 08 : "()V"
0x0001 : 屬性數量=1
0x0009 :"Code"
0x0000 002b 屬性長度爲43
解析一個Code表
0000 :max_stack =0
0001 : max_local =1
0000 0001 : code_length =1
0xb1 : return(該方法返回void)
0x0000 異常表長度=0
0x0002 屬性表長度爲2
//第一個屬性表
0x000a : #10,LineNumberTable
0x0000 0006 : 屬性長度爲6
0x0001 : line_number_length = 1
0x0000 : start_pc =0
0x0008 : end_pc =8
//第二個屬性表
0x000b : #11 ,LocalVariableTable
0x0000 000c : 屬性長度爲12
0x0001 : local_variable_table_length =1
0x0000 :start_pc = 0
0x0001: length = 1
0x000c : name_index =#12 "this"
0x000d : 描述索引#13 "Lcom/demo/Demo;"
0000 index=0

//到這裏爲止,方法解析都完成了,回過頭看看頂部解析順序圖,我們接下來就要解析Attributes了。

3.10 Attribute

另話

其實,我們寫了這麼多確實很麻煩,不過這種過程自己體驗一遍的所獲所得還是不同的。現在,使用java自帶的反編譯器來解析字節碼文件。
javap -verbose Demo //不用帶後綴.class

總結

到此爲止,講解完成了class文件的解析,這樣以後我們也能看懂字節碼文件了。瞭解class文件的結構對後面進一步瞭解虛擬機執行引擎非常重要,所以這是基礎並重要的一步。

作者博客

http://www.jianshu.com/u/b997cd1b6a9d