也許你寫了無數行的代碼,也許你能很是溜的使用高級語言,可是你未必瞭解那些高級語言的執行過程。例如大行其道的Java。java
Java號稱是一門「一次編譯處處運行」的語言,可是咱們對這句話的理解深度又有多少呢?從咱們寫的java文件到經過編譯器編譯成java字節碼文件(也就是.class文件),這個過程是java編譯過程;而咱們的java虛擬機執行的就是字節碼文件。不論該字節碼文件來自何方,由哪一種編譯器編譯,甚至是手寫字節碼文件,只要符合java虛擬機的規範,那麼它就可以執行該字節碼文件。那麼本文主要講講java字節碼文件相關知識。接下來咱們經過具體的Demo來深刻理解:安全
javasrc.pngoracle
上面是咱們寫的一個java程序,很簡單,只有一個成員變量a以及一個方法testMethod() 。jvm
demo.pngide
上圖是編譯好的字節碼文件,咱們能夠看到一堆16進制的字節。若是你使用IDE去打開,也許看到的是已經被反編譯的咱們所熟悉的java代碼,而這纔是純正的字節碼,這也是咱們今天須要講的內容重點。工具
也許你會對這樣一堆字節碼感到頭疼,不過不要緊,咱們慢慢試着你看懂它,或許有不同的收穫。在開始以前咱們先來看一張圖網站
java_byte.jpegthis
這張圖是一張java字節碼的總覽圖,咱們也就是按照上面的順序來對字節碼進行解讀的。一共含有10部分,包含魔數,版本號,常量池等等,接下來咱們按照順序一步一步解讀。spa
從上面的總覽圖中咱們知道前4個字節表示的是魔數,對應咱們Demo的是 0XCAFE BABE。什麼是魔數?魔數是用來區分文件類型的一種標誌,通常都是用文件的前幾個字節來表示。好比0XCAFE BABE表示的是class文件,那麼有人會問,文件類型能夠經過文件名後綴來判斷啊?是的,可是文件名是能夠修改的(包括後綴),那麼爲了保證文件的安全性,講文件類型寫在文件內部來保證不被篡改。
從java的字節碼文件類型咱們看到,CAFE BABE翻譯過來是咖啡寶貝之意,而後再看看java圖標。翻譯
java_icon.png
CAFE BABE = 咖啡。
咱們識別了文件類型以後,接下來要知道版本號。版本號含主版本號和次版本號,都是各佔2個字節。在此Demo種爲0X0000 0033。其中前面的0000是次版本號,後面的0033是主版本號。經過進制轉換獲得的是次版本號爲0,主版本號爲51。
從oracle官方網站咱們可以知道,51對應的正式jdk1.7,而其次版本爲0,因此該文件的版本爲1.7.0。若是須要驗證,能夠在用java --version命令輸出版本號,或者修改編譯目標版本--target從新編譯,查看編譯後的字節碼文件版本號是否作了相應的修改。
至此,咱們共瞭解了前8字節的含義,下面講講常量池相關內容。
緊接着主版本號以後的就是常量池入口。常量池是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的字節碼進行逐一翻譯。
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"
到這裏爲止咱們解析了全部的常量。接下來是解析訪問標誌位。
訪問標誌信息包括該Class文件是類仍是接口,是否被定義成public,是不是abstract,若是是類,是否被聲明成final。經過上面的源代碼,咱們知道該文件是類而且是public。
access_flag.png
0x 00 21:是0x0020和0x0001的並集。其中0x0020這個標誌值涉及到了字節碼指令,後期會有專題對字節碼指令進行講解。期待中......
類索引用於肯定類的全限定名
0x00 03 表示引用第3個常量,同時第3個常量引用第19個常量,查找得"com/demo/Demo"。#3.#19
0x00 04 同理:#4.#20(java/lang/Object)
經過java_byte.jpeg圖咱們知道,這個接口有2+n個字節,前兩個字節表示的是接口數量,後面跟着就是接口的表。咱們這個類沒有任何接口,因此應該是0000。果不其然,查找字節碼文件獲得的就是0000。
字段表用於描述類和接口中聲明的變量。這裏的字段包含了類級別變量以及實例變量,可是不包括方法內部聲明的局部變量。
一樣,接下來就是2+n個字段屬性。咱們只有一個屬性a,按道理應該是0001。查找文件果不其然是0001。
那麼接下來咱們要針對這樣的字段進行解析。附上字段表結構圖
字段表結構.png
0x00 02 :訪問標誌爲private(自行搜索字段訪問標誌)
0x00 05 : 字段名稱索引爲#5,對應的是"a"
0x 00 06 :描述符索引爲#6,對應的是"I"
0x 00 00 :屬性表數量爲0,所以沒有屬性表。
tips:一些不過重要的表(字段,方法訪問標誌表)能夠自行搜索,這裏就不貼出來了,防止篇幅過大。
咱們只有一個方法testMethod,按照道理應該前2個字節是0001。經過查找發現是0x00 02。這是什麼緣由,這表明着有2個方法呢?且繼續看......
方法表結構.png
上圖是一張方法表結構圖,按照這個圖咱們分析下面的字節碼:
0x00 01:訪問標誌 ACC_PUBLIC,代表該方法是public。(可自行搜索方法訪問標誌表)
0x00 07:方法名索引爲#7,對應的是"<init>"
0x00 08:方法描述符索引爲#8,對應的是"()V"
0x00 01:屬性表數量爲1(一個屬性表)
那麼這裏涉及到了屬性表。什麼是屬性表呢?能夠這麼理解,它是爲了描述一些專有信息的,上面的方法帶有一張屬性表。全部屬性表的結構以下圖:
一個u2的屬性名稱索引,一個u2的屬性長度加上屬性長度的info。
虛擬機規範預約義的屬性有不少,好比Code,LineNumberTable,LocalVariableTable,SourceFile等等,這個網上能夠搜索到。
屬性表結構.png
按照上面的表結構解析獲得下面信息:
0x0009:名稱索引爲#9("Code")。
0x000 00038:屬性長度爲56字節。
那麼接下來解析一個Code屬性表,按照下圖解析
code.png
前面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"
LineNumberTable.png
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"
local_variable_table.png
local_variable_info.png
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)接下來解析第二個方法
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了。
0x0001 :一樣的,表示有1個Attributes了。
0x000f : #15("SourceFile")
0x0000 0002 attribute_length=2
0x0010 : sourcefile_index = #16("Demo.java")
SourceFile屬性用來記錄生成該Class文件的源碼文件名稱。
source_file.jpeg
其實,咱們寫了這麼多確實很麻煩,不過這種過程本身體驗一遍的所獲所得仍是不一樣的。如今,使用java自帶的反編譯器來解析字節碼文件。
javap -verbose Demo //不用帶後綴.class
javap_result.png
到此爲止,講解完成了class文件的解析,這樣之後咱們也能看懂字節碼文件了。瞭解class文件的結構對後面進一步瞭解虛擬機執行引擎很是重要,因此這是基礎並重要的一步。