文章目錄
一、什麼是字節碼
1. 先來說一下什麼是機械碼
機械碼就是cpu能夠直接讀取並運行的代碼,它是用二進制編碼表示的,也叫做機械指令碼。在編寫這種代碼時,需要主動地去控制cpu的一切資源,而且需要記住全部指令所做的動作,十分的麻煩,當然這也是計算機的底層代碼,處理開發計算機的專業人員之外,已經很少人去研究了。
2.字節碼
字節碼是一種中間狀態的二進制文件,是由源碼編譯過來的,可讀性沒有源碼的高。cpu並不能直接讀取字節碼,在java中,字節碼需要經過JVM轉譯成機械碼之後,cpu才能讀取並運行。
3.使用字節碼的好處
一處編譯,到處運行。java就是典型的使用字節碼作爲中間語言,在一個地方編譯了源碼,拿着.class文件就可以在各種計算機運行,每個計算機上的jvm就會有所不同了。
4.字節碼在JVM中的狀態
5.額外提一點
編譯型語言
只需要編譯一次,就能夠將源代碼編譯成機械碼。執行效率高,可移植性低,依賴編譯器。
典型代表:C、C++、Pascal、Object-C以及最近很火的蘋果新語言swift,GO
解釋型語言
在第一次編譯時,並不會直接將源代碼編譯成機械碼,而是編譯成一種中間狀態的二進制文件(字節碼),由虛擬機來對這個二進制文件進行第二次編譯,這次纔是編譯成機械碼。執行效率比編譯型語言低,但是可移植性高,依賴虛擬機。
典型代表:JavaScript、Python、Erlang、PHP、Perl、Ruby
二、java中的字節碼
1.查看字節碼的方式
- 首先打開idea,在裏面創建一個.java文件
package test;
public class ByteCodeTest {
private int a = 0;
public int get() {
return a;
}
}
然後在另一個類上,運行main方法,調用這個類
- 找到編譯後的.class文件
out文件夾下面會多出一個我們剛剛編寫的java文件相同名稱的.class文件
- 下載一個Sublime Text,然後打開.class文件
cafe babe 0000 0034 0016 0a00 0400 1209
0003 0013 0700 1407 0015 0100 0161 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 134c 7465 7374 2f42
7974 6543 6f64 6554 6573 743b 0100 0367
6574 0100 0328 2949 0100 0a53 6f75 7263
6546 696c 6501 0011 4279 7465 436f 6465
5465 7374 2e6a 6176 610c 0007 0008 0c00
0500 0601 0011 7465 7374 2f42 7974 6543
6f64 6554 6573 7401 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0021 0003 0004
0000 0001 0002 0005 0006 0000 0002 0001
0007 0008 0001 0009 0000 0038 0002 0001
0000 000a 2ab7 0001 2a03 b500 02b1 0000
0002 000a 0000 000a 0002 0000 0003 0004
0004 000b 0000 000c 0001 0000 000a 000c
000d 0000 0001 000e 000f 0001 0009 0000
002f 0001 0001 0000 0005 2ab4 0002 ac00
0000 0200 0a00 0000 0600 0100 0000 0600
0b00 0000 0c00 0100 0000 0500 0c00 0d00
0000 0100 1000 0000 0200 11
2.一個疑惑
我也希望有大佬能夠解答一下我疑惑,我去查百度也找不到答案,可能是我搜索方式有問題。
上面是.class文件的十六進制形式
在idea中有這樣一個功能,view->Show ByteCode
用這個功能打開的是
// class version 52.0 (52)
// access flags 0x21
public class test/ByteCodeTest {
// compiled from: ByteCodeTest.java
// access flags 0x2
private I a
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 4 L1
ALOAD 0
ICONST_0
PUTFIELD test/ByteCodeTest.a : I
RETURN
L2
LOCALVARIABLE this Ltest/ByteCodeTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x1
public get()I
L0
LINENUMBER 6 L0
ALOAD 0
GETFIELD test/ByteCodeTest.a : I
IRETURN
L1
LOCALVARIABLE this Ltest/ByteCodeTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
我想知道這個跟十六進制文件的區別是什麼?
它們倆是怎麼轉換的?
三、java字節碼的組成
1.基本數據類型
數據類型 | 含義 |
---|---|
u1 | 無符號單字節整數 |
u2 | 無符號2字節整數 |
u4 | 無符號4字節整數 |
u8 | 無符號8字節整數 |
1Byte=8bit,在十六進制中,需要用兩位數來表示1Byte。
一個十六進制數需要4bit來表示。
2.java字節碼的格式
類型 | 數量 | 名稱 | 含義 |
---|---|---|---|
u4 | 1 | magic | 魔數 |
u2 | 1 | minor_version | 副版本號 |
u2 | 1 | major_version | 主版本號 |
u2 | 1 | constant_pool_count | 常量數 |
cp_info | constant_pool_count-1 | constant_pool | 常量池列表 |
u2 | 1 | access_flags | 訪問標記 |
u2 | 1 | this_class | 當前類 |
u2 | 1 | super_class | 父類 |
u2 | 1 | interfaces_count | 實現的接口數 |
u2 | interfaces_count | interfaces | 接口列表 |
u2 | 1 | fields_count | 字段個數 |
field_info | fields_count | fields | 字段列表 |
u2 | 1 | methods_count | 方法個數 |
method_info | methods_count | methods | 方法列表 |
u2 | 1 | attribute_count | 屬性個數 |
attribute_info | attributes_vount | attributes | 屬性列表 |
3.格式解讀
爲了節省空間,java對字節碼的格式有嚴格要求,所以我們能夠照着這個格式表來對字節碼進行解讀。
非基礎數據類型的類型其實也是有基礎數據類型來組成的,也是嚴格按照一定的格式來存放數據的。
可以看到常量池、接口、字段、方法、屬性都是採用數量+數據的格式進行存儲的。
四、解讀字節碼
以上面我們創建的ByteCodeTest.class文件爲例。
1.魔數(magic)
cafe babe
這個數使用來表示當前文件類型的,這個是有java之父James Gosling設定的。在代碼內部也有魔數,一般被叫做魔法值,一般是指在方法內部的常量值。
2.版本號(version)
0000 0034
副版本爲0,主版本爲52
對應java1.8(8),這個需要根據住版本跟副版本去查詢。
3.常量池(constant_pool)
常量池中存儲的是不會發生變化的數據。
常量池基本類型
常量個數(constant_pool_count)
0016
0x16=22
這裏指定了常量的個數,常量的個數爲22,#0~#22,實際個數爲21
爲什麼要減一我也不是很懂,有的說是因爲#0不作爲常量,有的說#0表示什麼都不引用。
常量池列表(pool_count)
在觀察常量時,需要先根據開頭的一個字節判斷它是什麼類型,然後才能知道它的長度。
#1
0a00 0400 12
0x0a=10,對應地找到了CONSTANT_Methodref_info
這個類型會引用兩個u2(2bit),也就是8位16進制
所以這裏是10個十六進制數表示一個常量
0x0004=4
0x0012=12
所以這個常量引用了#4、#12
全部常量
0a00 0400 12
09 0003 0013
0700 14
07 0015
0100 0161
0100 0149
0100 063c 696e 6974 3e
01 0003 2829 56
01 0004 436f 6465
0100 0f4c 696e 654e 756d 6265 7254 6162 6c65
0100 124c 6f63 616c 5661 7269 6162 6c65 5461 626c 65
01 0004 7468 6973
0100 134c 7465 7374 2f42 7974 6543 6f64 6554 6573 743b
0100 0367 6574
0100 0328 2949
0100 0a53 6f75 7263 6546 696c 65
01 0011 4279 7465 436f 6465 5465 7374 2e6a 6176 61
0c 0007 0008
0c00 0500 06
01 0011 7465 7374 2f42 7974 6543 6f64 6554 6573 74
01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374
每一行都代表一個常量
類型爲CONSTANT_UTF-8_info還需要另查ACSII碼錶
4.訪問標記(access_flags)
訪問標記
標記類型
標誌名稱 | 值(16進制) | 位(bit) | 描述 |
---|---|---|---|
PUBLIC | 0x0001 | 0000000000000001 | 對應public類型的類 |
PRIVATE | 0x0002 | 0000000000000010 | 字段爲private |
PROTECTED | 0x0004 | 0000000000000100 | 字段爲protected |
STATIC | 0x0008 | 0000000000001000 | 字段爲static |
FINAL | 0x0010 | 0000000000010000 | 對應類的final聲明 |
SUPER | 0x0020 | 0000000000100000 | 標識JVM的invokespecial新語義 |
VOLATILE | 0x0040 | 0000000000100000 | 字段是否爲volatile |
TRANSIENT | 0x0080 | 0000000001000000 | 字段是否爲transient |
INTERFACE | 0x0200 | 0000001000000000 | 接口標誌 |
ABSTRACT | 0x0400 | 0000010000000000 | 抽象類標誌 |
SYNTHETIC | 0x1000 | 0001000000000000 | 標識這個類並非用戶代碼產生 |
ANNOTATION | 0x2000 | 0010000000000000 | 標識這是一個註解 |
ENUM | 0x4000 | 0100000000000000 | 標識這是一個枚舉 |
訪問標記是根據每個bit上的0/1來標記的,從表中可以看出它是以16bit來表示的。
0021
訪問標記並不是直接對着表找,就可以找到是屬於那個類型的。
0x0021=0000000000100001,可以對照表格找到,它在PUBLIC和SUPER上,所以這個類具有public和super標誌
5.當前類(this_class)
當前類
表示指定在常量池的位置
0003
0x0003=3
說明當前類對應#3,也就是
#3
0700 14
這個又指向#20
#20
01 0011 7465 7374 2f42 7974 6543 6f64 6554 6573 74
acsii碼錶查詢結果:test/ByteCodeTest
6.父類(super_class)
當前類的父類
表示指定在常量池的位置
0004
0x0004=4
#4
07 0015
這個又指向#21
#21
01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374
acsii碼錶查詢結果:java/lang/Object
這裏可以看出,這個類繼承了Object類,所有類都繼承這個類,所以我們沒寫,它也繼承了。
7.接口(interfaces)
當前類實現的接口
接口數量(interfaces_count)
0000
我這裏沒有實現任何接口,當然接口數量爲0啦。
接口列表(interfaces)
如果有接口的話,後面會接interfaces_count* 4位16進制數,每個u2對應這常量池中的位置
8.字段(fields)
字段是指當前類的屬性,不是方法內部的屬性
字段個數(fields_count)
0001
說明這個類有一個屬性
然後我們讀取後面的16位16進制數
字段列表(fields)
字段類型
表示字符 | 含義 |
---|---|
B | byte字節類型 |
J | long長整型 |
C | char字符類型 |
S | short短整型 |
D | double雙精度浮點 |
Z | boolean布爾型 |
F | float單精度浮點 |
V | void類型 |
I | int整型 |
L | 對象引用類型 |
字段
0002 0005 0006 0000
第一個u2:字段的標記類型,標記類型,需要翻看前面的標記類型
第二個u2:字段的名稱,對應這常量池中的位置
第三個u2:字段的類型,對應這常量池中的位置,需要翻看字段的類型
第四個u2:字段的屬性,對應這常量池中的位置
0002說明這個字段爲private類型
0005指向常量池#5
#5
0100 0161
acsii碼錶查詢結果:a
0006指向常量池#6
#6
0100 0149
acsii碼錶查詢結果:I
I對應着int整型
0000指向#0,表示不作索引,也就是爲null
如果在定義該屬性時有賦值(int類型0是默認值),這個u2會指向一個不爲null的常量
拼起來就是private int a;
9.方法(methods)
當前類的方法
方法個數(methods_count)
0002
有兩個方法,然而我們只定義了一個方法,那另外一個方法是哪裏來的呢?
我們可以直接用idea打開編譯好的.class文件,就可以看到,另外一個方法是構造方法
方法(methods)
我們先往後讀6*4位16進制數
方法的描述
0001 0007 0008 0001
第一個u2:方法的標記類型,標記類型,需要翻看前面的標記類型
第二個u2:方法的名稱,對應這常量池中的位置
第三個u2:方法的類型,對應這常量池中的位置,需要翻看字段的類型
第四個u2:方法的屬性個數
翻譯過來就是 public ()V
有一個屬性
方法的屬性
我們需要往後讀3*4位16進制數,這幾位數說明了該方法的屬性情況
0009 0000 0038
第一個u2:屬性的名稱,對應這常量池中的位置
第二個u4:屬性描述的長度,表示後面的u2個數,都是對屬性的描述
第一個u2指向#9
#9
01 0004 436f 6465
acsii碼錶查詢結果:Code
這個Code是JVM虛擬機已經預定義好的屬性,相當於方法內部的代碼,詳情去百度搜一下“JVM虛擬機規範預定義的屬性”,這裏我就不展開講述了,我看到不是很懂。
第二個u2:0x38=56
那我們再往後讀56*2位16進制數
0002 0001 0000 000a 2ab7 0001 2a03
b500 02b1 0000 0002 000a 0000 000a
0002 0000 0003 0004 0004 000b 0000
000c 0001 0000 000a 000c 000d 0000
Code的屬性結構
第一個u2:屬性的最大堆數
第二個u2:屬性的最大本地內存
第三個u4:指令描述的長度,表示後面的u2個數
第四個n*u2:指令,需要參照JVM 虛擬機字節碼指令表
第五個u2:異常處理
第六個u2:屬性的屬性個數
·······後面就是屬性的描述了
屬性的解讀跟前面的屬性解讀一樣,但是需要注意的是,這些屬性一般都是JVM虛擬機已經預定義好的屬性,所以要按照相應的屬性結構進行解讀。
這裏我就不解讀了,
10.類屬性
這個就是當前類的屬性了
最後的幾位16進制數就是對類屬性的描述了
屬性的個數
00 01
表示有一個屬性
屬性的描述
00 1000 0000 0200 11
第一個u2:屬性常量的索引,對應這常量池中的位置
第二個u4:屬性描述的長度,表示後面的u2個數
n*u2:對應這常量池中的位置
五、總結
.class文件的結構
魔數
cafe babe
版本號
0000 0034
常量池
0016
0a00 0400 12
09 0003 0013
0700 14
07 0015
0100 0161
0100 0149
0100 063c 696e 6974 3e
01 0003 2829 56
01 0004 436f 6465
0100 0f4c 696e 654e 756d 6265 7254 6162 6c65
0100 124c 6f63 616c 5661 7269 6162 6c65 5461 626c 65
01 0004 7468 6973
0100 134c 7465 7374 2f42 7974 6543 6f64 6554 6573 743b
0100 0367 6574
0100 0328 2949
0100 0a53 6f75 7263 6546 696c 65
01 0011 4279 7465 436f 6465 5465 7374 2e6a 6176 61
0c 0007 0008
0c00 0500 06
01 0011 7465 7374 2f42 7974 6543 6f64 6554 6573 74
01 0010 6a61 7661 2f6c616e 672f 4f62 6a65 6374
當前類的訪問標記
0021
當前類
0003
父類
0004
實現接口數
0000
字段
0001
0002 0005 0006 0000
方法
方法個數
0002
方法描述
0001 0007 0008 0001
方法屬性描述
0009 0000 0038
0002 0001 0000 000a 2ab7 0001 2a03
b500 02b1 0000 0002 000a 0000 000a
0002 0000 0003 0004 0004 000b 0000
000c 0001 0000 000a 000c 000d 0000
0001 000e 000f 0001
0009 0000 002f
0001 0001 0000 0005 2ab4 0002 ac00
0000 0200 0a00 0000 0600 0100 0000
0600 0b00 0000 0c00 0100 0000 0500
0c00 0d00 00
類屬性
00 0100 1000 0000 0200 11
啓示
通過這次字節碼的學習,我瞭解到了字節碼的組成,java源代碼是怎麼編譯成.class文件的。
但是這個可真難啊,什麼都是規定死了的,只要對着結構表就可以解讀了。
——————————————————————————————
如果本文章內容有問題,請直接評論或者私信我。
我看到一個用JSON表示的.class文件的結構,我覺得特別不錯,篇幅太長了,但是篇幅太長了,我只好再創一片文章來展示了。