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