【JVM】帶你解讀字節碼

一、什麼是字節碼

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.查看字節碼的方式

  1. 首先打開idea,在裏面創建一個.java文件
package test;

public class ByteCodeTest {
    private int a = 0;
    public int get() {
        return a;
    }
}

然後在另一個類上,運行main方法,調用這個類

  1. 找到編譯後的.class文件
    out文件夾下面會多出一個我們剛剛編寫的java文件相同名稱的.class文件
    在這裏插入圖片描述
  2. 下載一個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文件的結構,我覺得特別不錯,篇幅太長了,但是篇幅太長了,我只好再創一片文章來展示了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章