带你看明白class二进制文件!
目錄
一、虛擬機的基石:Class文件
1.字節(jié)碼文件里是什么?
2.什么是字節(jié)碼指令(byte?code)?
3.如何解讀供虛擬機解釋執(zhí)行的二進制字節(jié)碼?
二、Class文件結(jié)構(gòu)
1.官方文檔位置:
2.class類的本質(zhì)
3.Class文件格式
4.代碼舉例
5.Class文件結(jié)構(gòu)概述
6.class文件詳細解析圖
三、魔數(shù):Class文件的標志
Magic?Number(魔數(shù))
四、Class文件版本號
五、常量池:存放所有常量
constant_pool_count(常量池計數(shù)器)
constant_pool[] (常量池)
常量池——字面量和符號引用
常量池——常量類型和結(jié)構(gòu)
六、訪問標識(access_flag、訪問標志、訪問標記)
補充說明
七、類索引、父類索引、接口索引集合
this_class(類索引)
super_class(父類索引)
interfaces
interfaces_count(接口計數(shù)器)
interfaces[](接口索引集合)
八、字段表集合
fields
注意事項
fields_count(字段計數(shù)器)
fields[](字段表)
字段訪問標識
字段名索引
描述符索引
屬性表集合
九、方法表集合
methods_count(方法計數(shù)器)
methods[](方法表)
十、 屬性表集合(attribute)
attributes_count(屬性計數(shù)器)
attributes[](屬性表)
屬性的通用格式
屬性類型
部分屬性詳解
十一、小結(jié)
一、虛擬機的基石:Class文件
1.字節(jié)碼文件里是什么?
? ? 源代碼經(jīng)過編譯器編譯之后便會生成一個字節(jié)碼文件,字節(jié)碼是一種二進制的類文件,它的內(nèi)容是JVM的機器碼指令,而不像C、C++經(jīng)由編譯器直接生成機器碼。
2.什么是字節(jié)碼指令(byte?code)?
? ? Java虛擬機的指令由一個字節(jié)長度的、代表著某種特定操作含義的操作碼(opcode)以及跟隨其后的零至多個代表此操作的所需參數(shù)的操作數(shù)(operand)所構(gòu)成。虛擬機中許多指令并不包含操作數(shù),只有一個操作碼。
3.如何解讀供虛擬機解釋執(zhí)行的二進制字節(jié)碼?
方式一:一個一個二進制的看。這里用到的是Notepad++,需要安裝一個HEX-Editor插件。(或者用UE)
方式二:使用javap指令:jdk自帶的反解析工具
javap -v xxx.class
方式三:使用IDEA插件:jclasslib或jclasslib?bytecode?viewer客戶端工具。(可視化更好)
或
二、Class文件結(jié)構(gòu)
1.官方文檔位置:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
2.class類的本質(zhì)
? ? 任何一個Class文件都對應(yīng)著唯一一個類或接口的定義信息,但反過來說,Class文件實際上它并不一定以磁盤文件的形式存在。Class文件是一組8位字節(jié)為基礎(chǔ)單位的二進制流。
3.Class文件格式
? ? Class的結(jié)構(gòu)不像XML等描述語言,由于它沒有任何分割符號。所以在其中的數(shù)據(jù)項,無論是字節(jié)順序還是數(shù)量,都是被嚴格限定的,哪個字節(jié)代表什么含義,長度是多少,先后順序如何,都不允許改變。
? ? Class文件格式采用一種類似于C語言結(jié)構(gòu)體的方式進行數(shù)據(jù)存儲,這種結(jié)構(gòu)中只有兩種數(shù)據(jù)類型:無符號數(shù)和表。
? ? ·?無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1、u2、u4、u8來分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù),無符號數(shù)可以用來描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值。
? ? ·?表是由多個無符號數(shù)或者其他表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型,所有表都習慣性地以“_info”結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),整個Class文件本質(zhì)上就是一張表。由于表沒有固定長度,所以通常會在其前面加上個數(shù)說明。
4.代碼舉例
public class Demo {private int num = 1;public int add() {num = num + 2;return num;} }javap -v Demo.class的結(jié)果:
換句話說,充分理解了每一個字節(jié)碼文件的細節(jié),自己也可以反編譯出Java源文件來。
5.Class文件結(jié)構(gòu)概述
? ? Class文件的結(jié)構(gòu)并不是一成不變的,隨著Java虛擬機的不斷發(fā)展,總是不可避免地會對Class文件結(jié)構(gòu)做出一些調(diào)整,但是其基本結(jié)構(gòu)和框架是非常穩(wěn)定的。
Class文件的總體結(jié)構(gòu)如下:
(1)魔數(shù)
(2)Class文件版本
(3)常量池
(4)訪問標志
(5)類索引,父類索引,接口索引集合
(6)字段表集合
(7)方法表集合
(8)屬性表集合
?
這是一張Java字節(jié)碼總的結(jié)構(gòu)表,我們按照上面的順序逐一進行解讀就可以了。
6.class文件詳細解析圖
三、魔數(shù):Class文件的標志
Magic?Number(魔數(shù))
1.每個Class文件開頭的4個字節(jié)的無符號整數(shù)稱為魔數(shù)(Magic?Number)。
2.它的唯一作用是確定這個文件是否為一個能被虛擬機接受的有效合法的Class文件。即:魔數(shù)是Class文件的標識符。
3.魔數(shù)值固定為0xCAFEBABE。不會改變。
4.如果一個Class文件不以0xCAFEBABE開頭,虛擬機在進行文件校驗的時候就會直接拋出以下錯誤:
5.使用魔數(shù)而不是擴展名來進行識別主要是基于安全方面的考慮,因為文件擴展名可以隨意地改動。
?
四、Class文件版本號
1.緊接著魔數(shù)的4個字節(jié)存儲的是Class文件的版本號。同樣也是4個字節(jié)。第5個和第6個字節(jié)所代表的的含義就是編譯的副版本號minor_version,而第7個和第8個字節(jié)就是編譯的主版本號major_version。
2.它們共同構(gòu)成了class文件的格式版本號。譬如某個Class文件的主版本號為M,副版本號為m,那么這個Class文件的格式版本號就確定為M.m。
3.版本號和Java編譯器的對應(yīng)關(guān)系如下表:
注意:下表為10進制,class文件用notepad++打開是16進制,需要做轉(zhuǎn)換。
4.Java的版本號是從45開始的,JDK1.1之后的每個JDK大版本發(fā)布主版本號向上加1。
5.不同的版本的Java編譯器編譯的Class文件對應(yīng)的版本是不一樣的。目前,高版本的Java虛擬機可以執(zhí)行由低版本編譯器生成的Class文件,但是低版本的Java虛擬機不能執(zhí)行由高版本編譯器生成的Class文件。否則JVM會拋出java.lang.UnsupportedClassVersionError異常。
6.在實際應(yīng)用中,由于開發(fā)環(huán)境和生產(chǎn)環(huán)境的不同,可能會導(dǎo)致該問題的發(fā)生。因此,需要我們在開發(fā)時,特別注意開發(fā)編譯的JDK版本和生產(chǎn)環(huán)境中的JDK版本是否一致。
? ? 虛擬機JDK版本為1.k(k>=2)時,對應(yīng)的class文件格式版本號的范圍為45.0 - 44+k.0(含兩端)。
?
五、常量池:存放所有常量
1.常量池是Class文件中內(nèi)容最為豐富的區(qū)域之一。常量池對于Class文件中的字段和方法解析也有著至關(guān)重要的作用。
2.隨著Java虛擬機的不斷發(fā)展,常量池的內(nèi)容也日漸豐富。可以說,常量池是整個Class文件的基石。
?
3.在版本號之后,緊跟著的是常量池的數(shù)量,以及若干個常量池表項。
4.常量池中的常量的數(shù)量是不固定的,所以在常量池的入口需要放置一項u2類型的無符號數(shù),代表常量池容量計數(shù)值(constant_pool_count)。與Java中語言習慣不一樣的是,這個容量計數(shù)是從1而不是0開始的。
?? ?由上表可見,Class文件使用了一個前置的容量計數(shù)器(constant_pool_count)加若干個連續(xù)的數(shù)據(jù)項(constant_pool)的形式來描述常量池內(nèi)容。我們把這一系列連續(xù)常量池數(shù)據(jù)稱為常量池集合。
? ? 常量池表項中,用于存放編譯時期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
?
constant_pool_count(常量池計數(shù)器)
1.由于常量池的數(shù)量不固定,時長時短,所以需要放置兩個字節(jié)來表示常量池容量計數(shù)值。
2.常量池容量計數(shù)值(u2類型):從1開始,表示常量池有多少項常量。即constant_pool_count=1表示常量池中有0個常量項。
3.Demo的值為:
?? ?其值為0x0016,掐指一算,也就是22。
? ? 需要注意的是,這實際上只有21項常量。索引范圍是1-21.為什么呢?
? ? 通常我們寫代碼時都是從0開始的,但是這里的常量池卻是從1開始,因為它把第0項常量空出來了。這是為了滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達“不引用任何一個常量池項目”的含義,這種情況可用索引值0來表示。
?
constant_pool[] (常量池)
1.constant_pool是一種表結(jié)構(gòu),以1 ~ constant_pool_count -1為索引。表明了后面有多少個常量項。
2.常量池主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic?References)。
3.它包含了class文件結(jié)構(gòu)及其子結(jié)構(gòu)中引用的所有字符串常量、類或接口名、字段名和其他常量。常量池中的每一項都具備相同的特征。第1個字節(jié)作為類型標記,用于確定該項的格式,這個字節(jié)成為tag?byte(標記字節(jié)、標簽字節(jié))。
?
常量池——字面量和符號引用
? ? 在對這些常量解讀前,我們需要搞清楚幾個概念。
? ? 常量池主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic?References)。如下表:
| 常量 | 具體的常量 |
| 字面量 | 文本字符串 |
| 聲明為final的常量值 | |
| 符號引用 | 類和接口的全限定名 |
| 字段的名稱和描述符 | |
| 方法的名稱和描述符 |
1.全限定名
?? ?com/boot/test/Demo這個就是類的全限定名,僅僅是把包名的“.”替換成“/”,為了使連續(xù)的多個全限定名之間不產(chǎn)生混淆,在使用時最后一般會加入一個“;”表示全限定名結(jié)束。
2.簡單名稱
? ? 簡單名稱是指沒有類型和參數(shù)修飾的方法或者字段名稱,上面例子中的類的add()方法和num字段的簡單名稱分別是add和num。
3.描述符
? ? 描述符的作用是用來描述字段的數(shù)據(jù)類型、方法的參數(shù)列表(包括數(shù)量、類型以及順序)和返回值。根據(jù)描述符規(guī)則,基本數(shù)據(jù)類型(byte、char、double、float、int、long、short、boolean)以及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名來表示,詳見下表:
| 標志符 | 含義 |
| B | 基本數(shù)據(jù)類型byte |
| C | 基本數(shù)據(jù)類型char |
| D | 基本數(shù)據(jù)類型doublt |
| F | 基本數(shù)據(jù)類型float |
| I | 基本數(shù)據(jù)類型int |
| J | 基本數(shù)據(jù)類型long |
| S | 基本數(shù)據(jù)類型short |
| Z | 基本數(shù)據(jù)類型boolean |
| V | 代表void類型 |
| L | 對象類型,比如:Ljava/lang/Object; |
| [ | 數(shù)組類型,代表一維數(shù)組。比如:double[][][] is [[[D |
? ? 用描述符來描述方法時,按照先參數(shù)列表,后返回值的順序描述,參數(shù)列表按照參數(shù)的嚴格順序放在一組小括號“()”之內(nèi)。如方法java.lang.String toString()?的描述符為“() Ljava/lang/String;”,方法int?abc(int[] x, int y)的描述符為“([II)I”。
4.補充說明:
? ? 虛擬機在加載Class文件時才會進行動態(tài)鏈接,也就是說,Class文件中不會保存各個方法和字段的最終內(nèi)存布局信息,因此,這些字段和方法的符號引用不經(jīng)過轉(zhuǎn)換是無法直接被虛擬機使用的。當虛擬機運行時,需要從常量池中獲得對應(yīng)的符號引用,再在類加載過程中的解析階段將其替換為直接引用,并翻譯到具體的內(nèi)存地址中。
? ? 這里說明下符號引用和直接引用的區(qū)別與關(guān)聯(lián):
? ? ·?符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標并不一定已經(jīng)加載到了內(nèi)存中。
? ? ·?直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經(jīng)存在于內(nèi)存之中了。
?? ?
常量池——常量類型和結(jié)構(gòu)
? ? 常量池中每一項常量都是一個表,JDK1.7之后共有14種不同的表結(jié)構(gòu)數(shù)據(jù)。如下表格所示:
u1:一個字節(jié)。
u2:兩個字節(jié)。
標志位1的表示字符串,length表示字符串的字節(jié)長度。
1.根據(jù)上圖每個類型的描述我們也可以知道每個類型是用來描述常量池中哪些內(nèi)容(主要是字面量、符號引用)的。比如:CONSTANT_Integer_info是用來描述常量池中字面量信息的,而且只是整型字面量信息。
2.標志為15、16、18的常量項類型是用來支持動態(tài)語言調(diào)用的(jdk1.7時才加入的)。
3.常量池詳細分析過程:
?
?找ASCII碼可以使用工具來協(xié)助:
?
4.總結(jié)1:
(1)這14種表(或者常量項結(jié)構(gòu))的共同點是:表開始的第一位是一個u1類型的標志位(tag),代表當前這個常量項使用的是哪種表結(jié)構(gòu),即哪種常量類型。
(2)在常量池列表中,CONSTANT_Utf8_info常量項是一種使用改進過UTF-8編碼格式來存儲諸如文字字符串、類或者接口的全限定名、字段或者方法的簡單名稱以及描述符等常量字符串信息。
(3)這14種常量項結(jié)構(gòu)還有一個特點是,其中13個常量項占用的字節(jié)固定,只有CONSTABT_Utf8_info占用字節(jié)不固定,其大小由length決定。為什么呢?因為從常量池存放的內(nèi)容可知,其存放的是字面量和符號引用,最終這些內(nèi)容都會是一個字符串,這些字符串的大小是在編寫程序時才確定,比如你定義一個類,類名可以取長取短,所以在沒編譯前,大小不固定,編譯后,通過utf-8編碼,就可以知道其長度。
5.總結(jié)2:
(1)常量池:可以理解為Class文件之中的資源倉庫,它是Class文件結(jié)構(gòu)中與其他項目關(guān)聯(lián)最多的數(shù)據(jù)類型(后面的很多數(shù)據(jù)類型都會指向此處),也是占用Class文件空間最大的數(shù)據(jù)項目之一。
(2)常量池中為什么要包含這些內(nèi)容?
? ? Java代碼在進行javac編譯的時候,并不像C和C++那樣有“連接”這一步驟,而是在虛擬機加載Class文件的時候進行動態(tài)鏈接。也就是說,在Class文件中不會保存各個方法、字段的最終內(nèi)存布局信息,因此這些字段、方法的符號引用不經(jīng)過運行期轉(zhuǎn)換的話無法得到真正的內(nèi)存入口地址,也就無法直接被虛擬機使用。當虛擬機運行時,需要從常量池獲得對應(yīng)的符號引用,再在類創(chuàng)建時或運行時解析、翻譯到具體的內(nèi)存地址之中。關(guān)于類的創(chuàng)建和動態(tài)鏈接的內(nèi)容,在虛擬機類加載過程時再進行詳細講解。
六、訪問標識(access_flag、訪問標志、訪問標記)
1.在常量池后,緊跟著訪問標記。該標記使用兩個字節(jié)表示,用于識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義為public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final等。各種訪問標記如下所示:
| 標志名稱 | 標志值 | 含義 |
| ACC_PUBLIC | 0x0001 | 標志位public類型 |
| ACC_FINAL | 0x0010 | 標志被聲明為final,只有類可以設(shè)置 |
| ACC_SUPER | 0x0020 | 標志允許使用invokespecial字節(jié)碼指令的新語義,JDK1.0.2之后編譯出來的類的這個標志默認為真。(使用增強的方法調(diào)用父類方法) |
| ACC_INTERFACE | 0x0200 | 標志這是一個接口 |
| ACC_ABSTRACT | 0x0400 | 是否為abstract類型,對于接口或者抽象類來說,次標志值為真,其他類型為假 |
| ACC_SYNTHETIC | 0x1000 | 標志此類并非由用戶代碼產(chǎn)生(即:由編譯器產(chǎn)生的類,沒有源碼對應(yīng)) |
| ACC_ANNOTATION | 0x2000 | 標志這是一個注解 |
| ACC_ENUM | 0x4000 | 標志這是一個枚舉 |
2.類的訪問權(quán)限通常為ACC_開頭。
3.每一種類型的表示都是通過設(shè)置訪問標記的32位中的特定位來實現(xiàn)的。比如,若是public?final的類,則該標記位ACC_PUBLIC | ACC_FINAL。
4.使用ACC_SUPER可以讓類更準確地定位到父類的方法super.method(),現(xiàn)代編譯器都會設(shè)置并且使用這個標記。
補充說明
1.帶有ACC_INTERFACE標志的class文件表示的是接口而不是類,反之則表示的是類而不是接口。
(1)如果一個class文件被設(shè)置了ACC_INTERFACE標志,那么同時也得設(shè)置ACC_ABSTRACT標志。同時它不能再設(shè)置ACC_FINAL、ACC_SUPER或ACC_ENUM。
(2)如果沒有設(shè)置ACC_INTERFACE標志,那么這個class文件可以具有上表中除ACC_ANNOTATION外的其他所有標志。當然,ACC_FINAL和ACC_ABSTRACT這類互斥的標志除外。這兩個標志不得同時設(shè)置。
2.ACC_SUPER標志用于確定類或接口里面的invokespecial指令使用的是哪一種執(zhí)行語義。針對java虛擬機指令集的編譯器都應(yīng)當設(shè)置這個標志。對于java?SE 8及后續(xù)版本來說,無論class文件中這個標志的實際值是什么,也不管class文件的版本號是多少,Java虛擬機都認為每個class文件均設(shè)置了ACC_SUPER標志。
(1)ACC_SUPER標志是為了向后兼容由舊Java編譯器所編譯的代碼而設(shè)計的。目前的ACC_SUPER標志在由JDK1.0.2之前的編譯器所生成的access_flags中是沒有確定含義的,如果設(shè)置了該標志,那么Oracle的Java虛擬機實現(xiàn)會將其忽略。
3.ACC_SYNTHETIC標志意味著該類或接口是由編譯器生成的,而不是由源代碼生成的。
4.注解類型必須設(shè)置ACC_ANNOTATION標志。如果設(shè)置了ACC_ANNOTATION標志,那么也必須設(shè)置ACC_INTERFACE標志。
5.ACC_ENUM標志標明該類或其父類為枚舉類型。
6.表中沒有使用的access_flags標志是為未來擴充而預(yù)留的,這些預(yù)留的標志在編譯器中應(yīng)該設(shè)置為0,Java虛擬機實現(xiàn)也應(yīng)該忽略它們。
七、類索引、父類索引、接口索引集合
1.在訪問標記后,會指定該類的類別、父類類別以及實現(xiàn)的接口,格式如下:
| 長度 | 含義 |
| u2 | this_class |
| u2 | super_class |
| u2 | interfaces_count |
| u2 | interfaces[interfaces_count] |
2.這三項數(shù)據(jù)類確定這個類的繼承關(guān)系。
(1)類索引用于確定這個類的全限定名。
(2)父類索引用于確定這個類的父類的全限定名。由于Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的Java類都有父類,因此除了java.lang.Object外,所有Java的父類索引都不為0。
(3)接口索引集合就用來描述這個類實現(xiàn)了哪些接口,這些被實現(xiàn)的接口將按implements語句(如果這個類本身是一個接口,則應(yīng)當是extends語句)后的接口順序從左到右排列在接口索引集合中。
this_class(類索引)
1.字節(jié)無符號整數(shù),指向常量池的索引。它提供了類的全限定名,如com/boot/java1/Demo。this_class的值必須是對常量池表中某項的一個有效索引值。常量池在這個索引處的成員必須為CONSTANT_Class_info類型的結(jié)構(gòu)體,該結(jié)構(gòu)體表示這個class文件所定義的類或接口。
super_class(父類索引)
1.2字節(jié)無符號整數(shù),指向常量池的索引。它提供了當前類的父類的全限定名。如果我們沒有繼承任何類,其默認繼承的是java/lang/Object類。同時,由于Java不支持多繼承,所以其父類只有一個。
2.superclass指向的父類不能是final。
interfaces
1.指向常量池索引集合,它提供了一個符號引用到所有已實現(xiàn)的接口。
2.由于一個類可以實現(xiàn)多個接口,因此需要以數(shù)組形式保存多個接口的索引,表示接口的每個索引也是一個指向常量池的CONSTANT_Class(當然這里就必須是接口,而不是類)。
interfaces_count(接口計數(shù)器)
interfaces_count項的值表示當前類或接口的直接超接口數(shù)量。
interfaces[](接口索引集合)
? ? interfaces[]中每個成員的值必須是對常量池表中某項的有效索引值,它的長度為interfaces_count。每個成員interfaces[i]必須為CONSTANT_Class_info結(jié)構(gòu),其中0<=i<interfaces_count。在interfaces[]中,各成員所表示的接口順序和對應(yīng)的源代碼中給定的接口順序(從左至右)一樣,即interfaces[0]對應(yīng)的是源代碼中最左邊的接口。
八、字段表集合
fields
1.用于描述接口或類中聲明的變量。字段(field)包括類級變量以及實例級變量,但是不包括方法內(nèi)部、代碼塊內(nèi)部聲明的局部變量。
2.字段叫什么名字、字段被定義為什么數(shù)據(jù)類型,這些都是無法固定的,只能引用常量池中的常量來描述。
3.它指向常量池索引集合,它描述了每個字段的完整信息。比如字段的標識符、訪問修飾符(public、private或protected)、是類變量還是實例變量(static修飾符)、是否是常量(final修飾符)等。
注意事項
1.字段表集合中不會列出從父類或者實現(xiàn)的接口中繼承而來的字段,但有可能列出原本Java代碼之中不存在的字段。譬如在內(nèi)部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。
2.在Java語言中字段是無法重載的,兩個字段的數(shù)據(jù)類型、修飾符不管是否相同,都必須使用不一樣的名稱,但是對于字節(jié)碼來講,如果兩個字段的描述符不一致,那字段重名就是合法的。
fields_count(字段計數(shù)器)
1.fields_count的值表示當前class文件fields表的成員個數(shù)。使用兩個字節(jié)來表示。
2.fields表中每個成員都是一個field_info結(jié)構(gòu),用于表示該類或接口所聲明的所有類字段或者實例字段,不包括方法內(nèi)部聲明的變量,也不包括從父類或父接口繼承的那些字段。
fields[](字段表)
1.fields表中的每個成員都必須是一個fields_info結(jié)構(gòu)的數(shù)據(jù)項,用于表示當前類或接口中某個字段的完整描述。
2.一個字段的信息包括如下這些信息。這些信息中,各個修飾符都是布爾值,要么有,要么沒有。
(1)作用域(public、private、protected)
(2)是實例變量還是類變量(static修飾符)
(3)可變性(final)
(4)并發(fā)可見性(volatile修飾符,是否強制從主內(nèi)存讀寫)
(5)可否序列化(transient修飾符)
(6)字段數(shù)據(jù)類型(基本數(shù)據(jù)類型、對象、數(shù)組)
(7)字段名稱
3.字段表結(jié)構(gòu)
? ? 字段表作為一個表,同樣有他自己的結(jié)構(gòu):
| 類型 | 名稱 | 含義 | 數(shù)量 |
| u2 | access_flags | 訪問標志 | 1 |
| u2 | name_index | 字段名索引 | 1 |
| u2 | descriptor_index | 描述符索引 | 1 |
| u2 | attributes_count | 屬性計數(shù)器 | 1 |
| attribute_info | attributes | 屬性集合 | attributes_count |
字段訪問標識
? ? 我們知道,一個字段可以被各種關(guān)鍵字去修飾,比如:作用域修飾符(public、private、protected)、static修飾符、final修飾符、volatile修飾符等等。因此,其可像類的訪問標志那樣,使用一些標志來標記字段。字段的訪問標志有如下這些:
| 標志名稱 | 標志值 | 含義 |
| ACC_PUBLIC | 0x0001 | 字段是否為public |
| ACC_PRIVATE | 0x0002 | 字段是否為private |
| ACC_PROTECTED | 0x0004 | 字段是否為protected |
| ACC_STATIC | 0x0008 | 字段是否為static |
| ACC_FINAL | 0x0010 | 字段是否為final |
| ACC_VOLATILE | 0x0040 | 字段是否為volatile |
| ACC_TRANSTENT | 0x0080 | 字段是否為transient |
| ACC_SYNCHETIC | 0x1000 | 字段是否為編譯器自動產(chǎn)生 |
| ACC_ENUM | 0x4000 | 字段是否為enum |
字段名索引
? ? 根據(jù)字段名索引的值,查詢常量池中的指定索引項即可。
描述符索引
? ? 描述符的作用是用來描述字段的數(shù)據(jù)類型、方法的參數(shù)列表(包括數(shù)量、類型以及順序)和返回值。根據(jù)描述符規(guī)則,基本數(shù)據(jù)類型(byte、char、double、float、int、long、short、boolean)及代表無返回值的void類型都用一個大寫字符來表示,而對象則用字符L加對象的全限定名來表示,如下所示:
| 標志符 | 類型 | 含義 |
| B | byte | 有符號字節(jié)型數(shù) |
| C | char | Unicode字符,UTF-16編碼 |
| D | doublt | 雙精度浮點數(shù) |
| F | float | 單精度浮點數(shù) |
| I | int | 整形數(shù) |
| J | long | 長整數(shù) |
| S | short | 有符號短整數(shù) |
| Z | boolean | 布爾值true/false |
| L?Classname; | reference | 一個名為Classname的實例 |
| [ | reference | 一個一維數(shù)組 |
屬性表集合
? ? 一個字段還可能擁有一些屬性,用于存儲更多的額外信息。比如初始化值、一些注釋信息等。屬性個數(shù)存放在attribute_count中,屬性具體內(nèi)存存放在attributes數(shù)組中。
以常量屬性(final修飾)為例,結(jié)構(gòu)為: ConstantValue_attribute{u2 attribute_name_inedx;u4 attribute_length;u2 constantvalue_index; }? ? 說明:對于常量屬性而言,attribute_length值恒為2。
九、方法表集合
1.methods:指向常量池索引集合,它完整描述了每個方法的簽名。
(1)在字節(jié)碼文件中,每一個method_info項都對應(yīng)著一個類或者接口中的方法信息。比如方法的訪問修飾符(public、private或protected),方法的返回值類型以及方法的參數(shù)信息等。
(2)如果這個方法不是抽象的或者不是native的,那么字節(jié)碼中會體現(xiàn)出來。
(3)一方面,methods表只描述當前類或接口中聲明的方法,不包括從父類或父接口繼承的方法。另一方面,methods表有可能會出現(xiàn)由編譯器自動添加的方法,最典型的便是編譯器產(chǎn)生的方法信息(比如:類(接口)初始化方法<clinit>()和實例初始化方法<init>())。
2.使用注意事項:
? ? 在Java語言中,要重載(Overload)一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的特征簽名,特征簽名就是一個方法中各個參數(shù)在常量池中的字段符號引用的集合,也就是因為返回值不會包含在特征簽名之中,因為Java語言里無法僅僅依靠返回值的不同來對一個已有方法進行重載。但在Class文件格式中,特征簽名的范圍更大一些,只要描述符不是完全一致的兩個方法就可以共存。也就是說,如果兩個方法有相同的名稱和特征簽名,但返回值不同,那么也是可以合法共存于同一個class文件中。
? ? 也就是說,盡管Java語法規(guī)范并不允許在一個類或接口中聲明多個方法簽名相同的方法,但是和Java語法規(guī)范相反,字節(jié)碼文件中卻恰恰允許存放多個方法簽名相同的方法,唯一的條件就是這些方法之間的返回值不能相同。
methods_count(方法計數(shù)器)
? ? methods_count的值表示當前class文件methods表的成員個數(shù)。使用兩個字節(jié)來表示。
? ? methods表中每個成員都是一個method_info結(jié)構(gòu)。
methods[](方法表)
1.methods表中的每個成員都必須是一個method_info結(jié)構(gòu),用于表示當前類或接口中某個方法的完整描述。如果某個method_info結(jié)構(gòu)的access_flags項既沒有設(shè)置ACC_NATIVE標志也沒有設(shè)置ACC_ABSTRACT標志,那么該結(jié)構(gòu)中也應(yīng)包含實現(xiàn)這個方法所用的Java虛擬機指令。
2.method_info結(jié)構(gòu)可以表示類和接口中定義的所有方法,包括實例方法、類方法、實例初始化方法和類或接口初始化方法。
3.方法表的結(jié)構(gòu)實際跟字段表是一樣的,方法表結(jié)構(gòu)如下(跟字段表基本類似,可以互相參照):
| 類型 | 名稱 | 含義 | 數(shù)量 |
| u2 | access_flags | 訪問標志 | 1 |
| u2 | name_index | 方法名索引 | 1 |
| u2 | descriptor_index | 描述符索引 | 1 |
| u2 | attributes_count | 屬性計數(shù)器 | 1 |
| attribute_info | attributes | 屬性集合 | attributes_count |
十、 屬性表集合(attribute)
? ?
? ? 方法表集合之后的屬性表集合,指的是class文件所攜帶的輔助信息,比如該class文件的源文件的名稱。以及任何帶有RetentionPolicy.CLASS或者RetentionPolicy.RUNTIME的注解。這類信息通常被用于Java虛擬機的驗證和運行,以及Java程序的調(diào)試,一般無須深入了解。
? ? 此外,字段表、方法表都可以有自己的屬性表。用于描述某些場景專有的信息。
? ? 屬性表集合的限制沒有那么嚴格,不再要求各個屬性表具有嚴格的順序,并且只要不與已有的屬性名重復(fù),任何人實現(xiàn)的編譯器都可以向?qū)傩员碇袑懭胱约憾x的屬性信息,但Java虛擬機運行時會忽略掉它不認識的屬性。
attributes_count(屬性計數(shù)器)
? ? attributes_count的值表示當前class文件屬性表的成員個數(shù)。屬性表中每一項都是一個attribute_info結(jié)構(gòu)。
attributes[](屬性表)
? ? 屬性表的每個項的值必須是attribute_info結(jié)構(gòu)。屬性表的結(jié)構(gòu)比較靈活,各種不同的屬性只要滿足以下結(jié)構(gòu)即可。
屬性的通用格式
| 類型 | 名稱 | 數(shù)量 | 含義 |
| u2 | attribute_name_index | 1 | 屬性名索引 |
| u4 | attribute_length | 1 | 屬性長度 |
| u1 | info | attribute_length | 屬性表 |
即只需說明屬性的名稱以及占用位數(shù)的長度即可,屬性表具體的結(jié)構(gòu)可以去自定義。
屬性類型
? ? 屬性表實際上可以有很多類型,上面看到的Code屬性只是其中一種,Java8里面定義了23種屬性。
? ? 下面這些是虛擬機中預(yù)定義的屬性:
| 屬性名稱 | 使用位置 | 含義 |
| Code | 方法表 | java代碼編譯成的字節(jié)碼指令 |
| ConstantValue | 字段表 | final關(guān)鍵字定義的常量池 |
| Deprecated | 類、方法、字段表 | 被聲明為deprecated的方法和字段 |
| Exceptions | 方法表 | 方法拋出的異常 |
| EnclosingMethod | 類文件 | 僅當一個類為局部類或者匿名類時才能擁有這個屬性,這個屬性用于標識這個類所在的外圍方法 |
| InnerClass | 類文件 | 內(nèi)部類列表 |
| LineNumberTable | Code屬性 | Java源碼的行號與字節(jié)碼指令的對應(yīng)關(guān)系 |
| LocalVariableTable | Code屬性 | 方法的局部變量描述 |
| StachMapTable | Code屬性 | JDK1.6中新增的屬性,供新的類型檢查檢驗器檢查和處理目標方法的局部變量和操作數(shù)有所需要的類是否匹配 |
| Signature | 類、方法表、字段表 | 用于支持泛型情況下的方法簽名 |
| SourceFile | 類文件 | 記錄源文件名稱 |
| SourceDebugExtension | 類文件 | 用于存儲額外的調(diào)試信息 |
| Synthetic | 類、方法表、字段表 | 標志方法或字段為編譯器自動生成的 |
| LocalVariableTypeTable | 類 | 使用特征簽名代替描述符,是為了引入泛型語法之后能描述泛型參數(shù)化類型而添加 |
| RuntimeVisibleAnnotations | 類、方法表、字段表 | 為動態(tài)注解提供支持 |
| RuntimeInvisibleAnnotations | 類、方法表、字段表 | 用于指明哪些注解是運行時不可見的 |
| RuntimeVisibleParameterAnnotation | 方法表 | 作用與RuntimeVisibleParameterAnnotation屬性類似,只不過作用對象為方法 |
| RuntimeInVisibleParameterAnnotation | 方法表 | 作用與RuntimeInVisibleParameterAnnotation屬性類似,作用對象哪個為方法參數(shù) |
| AnnotationDefault | 方法表 | 用于記錄注解類元素的默認值 |
| BootstrapMethods | 類文件 | 用于保存invokeddynamic指令引用的引導(dǎo)方式限定符 |
或查看官網(wǎng)。https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
部分屬性詳解
1.ConstantValue屬性
? ? ConstantValue屬性表示一個常量字段的值。位于field_info結(jié)構(gòu)的屬性表中。
ConstantValue_attribute {u2 attribute_name_index;u4 attribute_length;u2 constantvalue_index; //?字段值在常量池中的索引,常量池在該索引處的項給出該屬性表示的常量值。(例如,值是long型的,在常量池中便是CONSTANT_Long)}2.Deprecated屬性
? ? Deprecated屬性是在JDK1.1為了支持注釋中的關(guān)鍵字@deprecated而引入的。
Deprecated_attribute {u2 attribute_name_index;u4 attribute_length;}3.Code屬性
? ? Code屬性就是存放方法體里面的代碼。但是,并非所有方法表都有Code屬性。像接口或者抽象方法,他們沒有具體的方法體,因此也就不會有Code屬性了。
? ? Code屬性表的結(jié)構(gòu),如下圖:
| 類型 | 名稱 | 數(shù)量 | 含義 |
| u2 | attribute_name_index | 1 | 屬性名索引 |
| u4 | attribute_length | 1 | 屬性長度 |
| u2 | max_stack | 1 | 操作數(shù)棧深度的最大值 |
| u2 | max_locals | 1 | 局部變量表所需的存儲空間 |
| u4 | code_length | 1 | 字節(jié)碼指令的長度 |
| u1 | code | code_length | 存儲字節(jié)碼指令 |
| u2 | exception_table_length | 1 | 異常表長度 |
| exception_info | exception_table | exception_length | 異常表 |
| u2 | attributes_count | 1 | 屬性集合計數(shù)器 |
| attribute_info | attributes | attributes_count | 屬性集合 |
?? ?可以看到:Code屬性表的前面兩項跟屬性表是一致的,即Code屬性表遵循屬性表的結(jié)構(gòu),后面那些則是他自定義的結(jié)構(gòu)。
4.InnerClasses屬性
? ? 為了方便說嗎特別定義一個表示類或接口的Class格式為C。如果C的常量池中包含某個CONSTANT_Class_info成員,且這個成員所表示的類或接口不屬于任何一個包,那么C的ClassFile結(jié)構(gòu)的屬性表中就必須含有對應(yīng)的InnerClasses屬性。InnerClasses屬性是在JDK1.1中為了支持內(nèi)部類和內(nèi)部接口而引入的,位于ClassFile結(jié)構(gòu)的屬性表。
5.LineNumberTable屬性
? ? LineNumberTable屬性是可選變長屬性,位于Code結(jié)構(gòu)的屬性表。
? ? LineNumberTable屬性是用來描述Java源碼行號與字節(jié)碼行號之間的對應(yīng)關(guān)系。這個屬性可以用來在調(diào)試的時候定位代碼執(zhí)行的行數(shù)。
? ? · start_pc,即字節(jié)碼行號;line_number,即java源代碼行號。
? ? 在Code屬性的屬性表中,LineNumberTable屬性可以按照任意順序出現(xiàn),此外,多個LineNumberTable屬性可以共同表示一個行號在源文件中表示的內(nèi)容,即LineNumberTable屬性不需要與源文件的行一一對應(yīng)。
? ? LineNumberTable屬性表結(jié)構(gòu):
LineNumberTable_attribute {u2 attribute_name_index;u4 attribute_length;u2 line_number_table_length;{u2 start_pc;u2 line_number;} line_number_table[line_number_table_length];}6.LocalVariableTable屬性
? ? LocalVariableTable是可選變長屬性,位于Code屬性的屬性表中。它被調(diào)試器用于確定方法在執(zhí)行過程中局部變量的信息。在Code屬性的屬性表中,LocalVariableTable屬性可以按照任意順序出現(xiàn)。Code屬性中的每個局部變量最多只能有一個LocalVariableTable屬性。
? ? ·?start?pc +?length表示這個變量在字節(jié)碼中的聲明周期起始和結(jié)束的偏移位置(this生命周期從頭0到結(jié)尾10)
? ? ·?index就是這個變量在局部變量表中的槽位(槽位可復(fù)用)
? ? ·?name就是變量名稱
? ? ·?Descriptor表示局部變量類型描述
LocalVariableTable屬性表結(jié)構(gòu):
LocalVariableTable_attribute {u2 attribute_name_index;u4 attribute_length;u2 local_variable_table_length;{u2 start_pc;u2 length;u2 name_index;u2 descriptor_index;u2 index;} local_variable_table[local_variable_table_length];}7.Signature屬性
? ? Signature屬性是可選的定長屬性,位于ClassFile、field_info或method_info結(jié)構(gòu)的屬性表中。在Java語言中,任何類、接口、初始化方法或成員的泛型簽名如果包含了類型變量(Type?Variables)或參數(shù)化類型(Parameterized?Types),則Signature屬性會為它記錄泛型簽名信息。
8.SourceFile屬性
SourceFile屬性結(jié)構(gòu):
| 類型 | 名稱 | 數(shù)量 | 含義 |
| u2 | attribute_name_index | 1 | 屬性名索引 |
| u4? | attribute_length | 1 | 屬性長度 |
| u2 | sourcefile_index | 1 | 源碼文件索引 |
可以看到,其長度總是固定的8個字節(jié)。
9.其他屬性
? ? Java虛擬機中預(yù)定義的屬性有20多個,這里就不一一介紹了,通過上面的幾個屬性介紹,只要領(lǐng)會其精髓,其他屬性的解讀也是易如反掌。
十一、小結(jié)
1.本章主要介紹了Class文件的基本格式。
2.隨著Java平臺的不斷發(fā)展,在將來,Class文件的內(nèi)容也一定會做進一步的擴充,但是其基本的格式和結(jié)構(gòu)不會做重大調(diào)整。
3.從Java虛擬機的角度看,通過Class文件,可以讓更多的計算機語言支持Java虛擬機平臺。因此,Class文件結(jié)構(gòu)不僅僅是Java虛擬機的執(zhí)行入口,更是Java生態(tài)圈的基礎(chǔ)和核心。
總結(jié)
以上是生活随笔為你收集整理的带你看明白class二进制文件!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jenkins搭建流水线项目
- 下一篇: 使用nexus3搭建maven私服(超详