Java Class 類文件格式看這一篇就夠了

本文將揭開Java Class文件的神秘面紗,帶你了解Class文件的內部結構,并從Class文件結構的視角告訴你:

  • 為什么Java Class字節碼文件可以“寫一次,遍地跑”?
  • 為什么常量池的計數從1開始,而不是和java等絕大多數語言的習慣一樣從0開始計數?
  • 為什么在Java應用運行期間,無法使用反射在普通對象中獲取到泛型信息?

平臺無關性

Java應用之所以能“Write once, Run anywhere”,是因為有JVM虛擬機這個中間媒介來執行Java程序,而JVM虛擬機不和包括Java在內的任何語言綁定,它只和“Class”文件這種約定的二進制文件格式所關聯。虛擬機通過載入和執行平臺無關的字節碼,從而實現了程序的“Write once, Run anywhere”。

什么是Class文件?

“深入理解Java虛擬機”一書中給出了定義,“Class文件是一組以8位字節為基礎單位的二進制流”。各個數據項目按照順序緊湊排列,中間沒有分隔符,整個Class文件沒有一點空間上的浪費。

利用idea插件BinEd打開Class文件,我們可以看到用十六進制表示的Class文件,開頭是固定的0xCAFEBABE(咖啡寶貝)魔數,它的唯一作用是用來驗證此文件是可以被虛擬機接受的Class文件,而不是通過后綴.class來驗證,因為后綴名是可以人為修改的。很多格式如gif或者jpeg等文件頭都存在魔數。

Class文件格式

Class文件格式按照虛擬機規范的約定,采用一種類似于C語言結構體的偽結構來存儲數據,只要各個平臺編譯器能夠嚴格遵守Java虛擬機的規范來生成Class文件,Java虛擬機就能生成它可執行的Class文件。在Class文件中只有兩種數據類型:

  • 無符號數。屬于基本的數據類型,u1、u2、u4和u8分別表示1個、2個、4個和8個字節的無符號數,它們可以用來描述數字、索引引用、數量值或者utf-8編碼的字符串。
  • 表。是由多個無符號數或其他表構成的復合數據結構。表習慣以“_info”結尾,整個Class文件本質上也是一張表,因為它也是具有層次關系的復合數據類型。

如上圖,當同一個數據類型有多個時,經常會在這個數據類型集合的前面加上一個計數器。例如,fields數據項表示Class文件里的多個field_info字段,其前面的fields_count保存了fields集合的數量。

下面正式開始介紹Class文件的各個數據項的含義和作用。

Class文件的版本

前面已經介紹了Class文件以魔數(magic)開頭,魔數是u4類型,占用了4個字節。緊隨其后的第5和第6個字節是次版本號(Minor Version),第7和第8個字節是主版本號(Major Version)。Java的版本號從45開始,從1.1開始每個JDK大版本的主版本號都向上加1(JDK 1.0~1.1使用了45.0~45.3的版本號)。例如,我的本地JDK版本是1.8,所以編譯的Class文件大版本號是十六進制0x0034,即十進制52。

版本號的作用是保證高版本的JDK向下兼容低版本的Class文件,但必須拒絕超過其版本號的Class文件。

常量池

主版本號之后是常量池入口,常量池是占用Class文件空間最大的數據項目之一,因為其他項目里存放的引用類型,是和常量池里存放的數據進行的關聯。例如,類索引(this_class)是u2類型的數據,里面存放的是引用常量池的地址,常量池對應的區域保存的是類的全限定名。

我們先用一個示例來大致的了解一下常量池的結構。以下是class文件對應的源代碼,我們可以使用idea插件jclasslib,打開這個類的class文件看看它的構造。

public class GuoClass<T> {private int money;public int make() {return money + 1000000000;}
}

在一般信息里,我們看到有一項“本類索引”(this_class),cp_info表示本類索引的數據類型是常量池類型(constant_pool_info),編號#4表示本類索引指向的是常量池的4號位置,它存放的數據是CONSTANT_Class_info類型。

點開常量池編號#4的索引,可以看到它存放的也是一個cp_info的數據,顯然我們可以再次點擊它跳轉到常量池編號25的位置看看。

果然,這里我們看到了常量池編號#25的位置存放的是一個字符串字面量,它保存的就是我們最終要找的類的全限定名“com/examples/test/GuoClass”。

通過上面的示例,我們可以分析出幾個重點:

  • 常量池的容量計數是從1開始算起的。這和絕大多數語言習慣包括Java都不太一樣。這樣做的目的在于滿足特定情況下,不引用常量池項目時,將索引值置為0。
  • 常量池的常量數量是不固定的。所以在常量池的前面需要放一個類型為u2的數據,代表常量池的容量。上面例子中constant_pool_count存放了十六進制數0x001B,即十進制27,代表常量池有(27-1 = 26)個常量,索引值范圍是1~26。

  • 常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)。字面量類似于Java語言的文本或final常量值。符號引用包括:類和接口的全限定名、字段的名稱和描述符以及方法的名稱和描述符。
  • 常量池是最繁瑣的數據,其中每一項常量都是一個表,除了第一位是一個u1類型的標志位(tag)以外,每一項常量均有各自的結構。如圖是常量池的項目類型。

  • 常量池每一項數據類型依靠標志位(tag)來區分。知道了常量池數據項的類型之后,就可以根據常量項的結構總表來查詢常量項的具體信息。

我們再來看一個例子,從總體上直觀感受一下常量池的結構。還是解析上面的GuoClass.class類文件,圖中紅框1圈出來的是常量池的第一位的數據項,對應的是紅框2圈出來的部分,它的數據類型是CONSTANT_Methodref_info,通過上圖我們知道:

CONSTANT_Methodref_info的第一部分是一個u1類型的標志位(tag),紅框1中的0x0A,即十進制10,正好對應標志位(tag)的枚舉值CONSTANT_Methodref_info。

CONSTANT_Methodref_info的第二部分是一個u2類型的索引項,紅框1中的0x0005,即十進制5,正好對應紅框3中的cp_info#5。

CONSTANT_Methodref_info的第三部分是一個u2類型的索引項,紅框1中的0x0017,即十進制23,正好對應紅框3中的cp_info#23。

訪問標志

如果你能看到這里,說明你已經跨過了學習Class文件格式最困難的部分,堅持下去一定會有收獲!

緊接著常量池的是訪問標志(access_flags),它用于識別類或接口層次的訪問信息,比如這個Class是類還是接口;是否為public類型;有沒有abstract修飾;有沒有final關鍵字等等。具體標志位如圖所示:

以GuoClass這個類為例,它是一個普通的Java類,不是接口、枚舉或者注解,public類型,沒有final和abstract關鍵字修飾,所以它的ACC_PUBLIC、ACC_SUPER標志應當為真,其他的標志位應該為假,所以它的access_flags的值應為:0x0001 | 0x0020=0x0021。從它的十六進制圖可以看出,我們的結果是正確的。

類索引、父類索引與接口索引集合

類索引、父類索引和接口索引集合都按順序排列在訪問標志之后。

  • 類索引(this_class),u2類型數據,用于確定類的全限定名。
  • 父類索引(super_class),u2類型數據,用于確定類的父類的全限定名。由于Java語言不允許多重繼承,因此父類索引只能有一個。除了java.lang.Object之外,所有的Java類都有父類,所以除了java.lang.Object之外,所有的Java類的父類索引都不為0。
  • 接口索引集合(interfaces),一組u2類型數據,用來描述這個類實現的接口。也就是這個類按implements語句后的接口順序排列的集合。如果當前類是一個接口,則應當是extends語句后的接口。

類索引、父類索引和接口索引的查找過程是一樣的,都是用u2類型的索引值表示,指向一個CONSTANT_Class_info類型的類描述符常量,再通過CONSTANT_Class_info類型的常量中的索引值找到CONSTANT_Utf8_info類型的全限定名字符串。

以GuoClass這個類為例,在access_flags之后的兩個字節是this_class,這個u2類型的數據項用十六進制表示為0x0004,它指向常量池中第4個類型為CONSTANT_Class_info的常量,再根據此常量里的索引值找到常量池中第25個位置保存的CONSTANT_Utf8_info類型的字符串,這個字符串就是我們需要找的全限定名“com/examples/test/GuoClass”。

在this_class之后,即0x0004后面的四個字節,0x0005和0x0000分別表示super_class和interfaces集合的入口。super_class和this_class的查找過程一摸一樣,而由于GuoClass沒有實現的接口,所以它的入口值是0x0000,即常量池的0位置,這也是前文提到過的為什么常量池從1開始計數的原因。0在不引用常量值的時候使用。

字段表集合

前文已經介紹了Class文件里的常量池、訪問標志和繼承關系(類索引、父類索引、接口索引集合),那么在一個Java類的還剩下什么信息沒有介紹呢?對!這一部分我們介紹類的字段。

字段(field)包括類變量和實例變量,但不包括方法內部聲明的局部變量。字段數據項的類型是字段表(field_info),它用于描述接口或類中聲明的變量。

字段表(field_info)結構

想象一下在Java里描述一個字段需要包含哪些信息?

  • 字段的作用域(public、private、protected)
  • 實例變量還是類變量(static)
  • 可變性(final)
  • 并發可見性(volatile)
  • 可否被序列化(transient)
  • 字段數據類型(基本類型、對象、數組)
  • 字段名稱

字段表結構如圖所示:

字段表的access_flags

字段表里的access_flags與類中的access_flags非常相似,它們都是u2類型,且都是訪問標志。

除了字段數據類型和字段名稱,其他的信息都是修飾符,都可以用布爾值來表示是否有某一個修飾符。字段訪問標志位如圖所示:

顯然,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED只能三選一;ACC_FINAL、ACC_VOLATILE只能二選一;接口中的字段必須有ACC_PUBLIC、ACC_STATIC、ACC_FINAL標志。

以GuoClass為例,緊隨上文中接口索引入口0x0000的是0x0001和0x0002。0x0001是字段表前面的fields_count數據項,fields_count用來對字段的數量計數,因為GuoClass只有一個int類型的字段money,所以fields_count作為一個u2類型的數據項,保存了兩個字節,用十六進制表示數字1就是0x0001。緊隨fields_count之后的就是字段表的access_flags數據項,它的值是0x0002,因為money字段是用private修飾的,所以對應的就是ACC_PRIVATE標志。

字段表的name_index和descriptor_index

緊接著access_flags標志的是兩個索引值:name_index和descriptor_index。

name_index代表字段的簡單名稱,如果是在方法表里代表的是方法的簡單名稱。例如,make()方法的簡單名稱是“make”,money字段的簡單名稱是“money”。

descriptor_index代表字段或方法的描述符。描述符是用來描述字段的數據類型、方法的參數列表和返回值。描述符的標識字符如圖所示:

當使用描述符描述數組類型時,使用一個前置的“[”,例如,一個整型數組可以被表示為“[I”,一個字符串類型的二位數組可以表示為“[[Ljava/lang/String;”。

當使用描述符描述方法時,按照先參數列表,再返回值的順序,參數列表按照參數順序放在一對小括號“()”里。例如,

  • 方法int make()的描述符為“()I”
  • 方法java.lang.String toString()的描述符為“()Ljava/lang/String;”
  • 方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符為“([CII[CIII)I”

以GuoClass為例,緊隨access_flags的是name_index,如圖十六進制0x0006;再來是descriptor_index,十六進制表示為0x0007。

我們可以把索引0x0006和0x0007在常量池中查找一下,0x0006保存的是一個CONSTANT_Utf8_info類型的字面量“money”,即字段的簡單名稱。

0x0007在常量池里保存為一個CONSTANT_Utf8_info類型的字面量“I”,即字段的描述符,表示money字段是int類型的。

至此,通過access_flags、name_index和descriptor_index查找到的信息,我們可以知道GuoClass里的字段信息是“private int money”。

descriptor_index之后還有一個屬性表集合用于保存一些額外的信息。例如,"final static int money = 100000;" 會存在一項名稱為ConstantValue的屬性,其值指向常量100000。但是本例中的字段money沒有額外信息,所以屬性計算器為0。

方法表集合

方法表的內容基本上可以參照字段表的內容,因為它們的結構幾乎一模一樣都包括了訪問標志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)這幾項。

方法表的access_flags

方法訪問標志的取值如圖所示。由于volatile和transient關鍵字不能修飾方法,所以在方法訪問標志里去掉了ACC_VOLATILE和ACC_TRANSIENT。因為synchronized、abstract、native和strictfp關鍵字可以修飾方法,所以增加了ACC_SYNCHRONIZED、ACC_ABSTRACT、ACC_NATIVE和ACC_STRICTFP標志。

方法表里只不會保存有具體的代碼信息,方法的java代碼被編譯器編譯成字節碼指令,保存在屬性表集合的“Code”屬性里。

public class GuoClass<T> {private int money;public int make() {return money + 1000000000;}
}

以GuoClass為例,方法表集合的第一個u2類型的數據是計數器容量,它的值為0x0002表示這個類文件有兩個方法,其中一個顯然就是代碼中的make()方法,另外一個比較隱蔽,是實例的構造器方法,構造器方法是編譯器自動添加的方法。構造器方法是public公有的,所以訪問標志是ACC_PUBLIC,對應的十六進制數是0x0001。

方法表的name_index和descriptor_index

緊挨著構造器方法的訪問標志位的是u2類型的方法名稱索引,其值為0x0008,在常量池中我們可以查詢到方法名稱為""。

再往后是u2類型的方法描述符索引,其值為0x0009,在常量池中我們可以查詢到方法描述為"()V"。前面我們已經提到過這個描述符的含義表示方法沒有參數,并且返回空void。

屬性表計數器的值為0x0001,表示屬性表集合有一個屬性。屬性名稱索引為0x000A,在常量池中查詢到其值為“Code”,說明此屬性是方法的字節碼描述。

方法重載

在Java語言中,要重載一個方法,除了方法名要相同外,方法參數的個數或類型不能一樣。但是在Class文件格式中,只要描述符不是完全一致的兩個方法也可以共存,即如果兩個方法具有相同的方法名,方法參數的個數和類型也一樣,但返回值類型不同,這兩個方法也是可以合法共存于同一個Class文件里的。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/160864.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/160864.shtml
英文地址,請注明出處:http://en.pswp.cn/news/160864.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

封裝Redis工具類

基于StringRedisTemplate封裝一個緩存工具類&#xff0c;滿足下列需求&#xff1a; 方法1&#xff1a;將任意Java對象序列化為json并存儲在string類型的key中&#xff0c;并且可以設置TTL過期時間 方法2&#xff1a;將任意Java對象序列化為json并存儲在string類型的key中&…

【JVM精講與GC調優教程(概述)】

如何理解虛擬機(JVM)跨語言的平臺 java虛擬機根本不關心運行在其內部的程序到底是使用何種編程語言編寫的,他只關心“字節碼”文件。 java不是最強大的語言,但是JVN是最強大的虛擬機。 不存在內存溢出? 內存泄露? JAVA = (C++)–; 垃圾回收機制為我們打理了很多繁瑣的…

力扣刷題第二十八天--二叉樹

前言 今天的五道題都是層序遍歷的模板&#xff0c;深度優先的遞歸還不太熟。繼續努力。 內容 一、在每個樹行中找最大值 515.在每個樹行中找最大值 給定一棵二叉樹的根節點 root &#xff0c;請找出該二叉樹中每一層的最大值。 廣度優先搜素 時間復雜度&#xff1a;O(n)…

算法基礎:KMP算法詳細詳解

目錄 1、幾個最基本的概念 2、暴力算法 3、KMP算法 4、KMP代碼實現 5、時間復雜度 1、幾個最基本的概念 字符串的前綴&#xff1a; 主串&#xff08;目標串&#xff09;從索引0開始的子串被稱為主串的前綴。 字符串的后綴&#xff1a; 主串從索引大于0的位置到結尾的子串…

【人工智能入門學習資料福利】

總目錄如下&#xff08;部分截取&#xff09;&#xff1a; 百度網盤鏈接&#xff1a;https://pan.baidu.com/s/1bfDVG-xcPR3f3nfBJXxqQQ?pwdifu6 提取碼&#xff1a; ifu6

Sentinel在Spring Cloud中的流量控制與熔斷降級:保障你的微服務穩定運行

在當今高度互聯的世界中&#xff0c;微服務架構已成為構建穩健系統的基石。然而&#xff0c;隨著系統復雜性的增加&#xff0c;高并發和異常情況下&#xff0c;保障服務穩定性變得尤為關鍵。本文將帶你探索Spring Cloud中Sentinel框架的強大功能&#xff0c;它能夠為你的微服務…

協程及運用

協程 使用方法一方法二網頁下載中使用有返回值 實戰圖片實戰 一個線程多個任務&#xff0c;線程由操作系統開啟&#xff0c;比較耗資源。線程內合理分配任務&#xff0c;充分利用線程內的資源&#xff0c;一個任務io阻塞時&#xff0c;cpu處理其他非阻塞任務。 使用 方法一 i…

B站已經部分上線前臺實名,如不同意實名,后期賬號流量將收影響!

B站部分百萬粉絲博主的主頁顯示賬號運營人名字的政策是從10月31日開始的。當天&#xff0c;B站官方發布了《嗶哩嗶哩關于頭部“自媒體”賬號前臺實名的公告》&#xff0c;表明了其前臺實名制的實施計劃。 B站部分上線前臺實名的過程可以追溯到2021年。當時&#xff0c;中國政府…

window下殺指定端口進程

netstat -ano | findstr "8762" taskkill /pid 14992 /f

【LeetCode】144. 二叉樹的前序遍歷

144. 二叉樹的前序遍歷 難度&#xff1a;簡單 題目 給你二叉樹的根節點 root &#xff0c;返回它節點值的 前序 遍歷。 示例 1&#xff1a; 輸入&#xff1a;root [1,null,2,3] 輸出&#xff1a;[1,2,3]示例 2&#xff1a; 輸入&#xff1a;root [] 輸出&#xff1a;[]…

ARM裸機-18(SD卡啟動)

1、主流的外存設備介紹 內存和外存的區別&#xff1a;一般是把這種RAM(random access memory&#xff0c;隨機訪問存儲器&#xff0c;特點是任意字節讀寫&#xff0c;掉電丟失)叫內存&#xff0c;把ROM (read only memory&#xff0c;只讀存儲器&#xff0c;類似于Flash、SD卡之…

如何解決安卓手機無法預覽pdf文件而是需要直接下載的問題

在開發中常常會遇到需要在一個應用里打開一份pdf文件并預覽&#xff0c;經真機調試時發現在蘋果手機上打開pdf文件能正常預覽&#xff0c;但在安卓手機打開時卻會需要我們下載才能預覽&#xff0c;無法直接預覽 為了解決這個問題&#xff0c;我們采用安裝pdfH5插件的方式&…

計算機三級嵌入式知識總結(一)

一、ARM的七種異常類型 1、復位異常RESET “復位異常RESET”通常是指在電子設備或系統中發生了一個意外的復位或重啟。這可能是由于硬件故障、軟件問題或其他未知的原因引起的。當設備經歷復位異常時&#xff0c;它可能會丟失正在進行的操作或設置&#xff0c;導致數據丟失或系…

LINUXZ

10.6.2 AT24C02 訪問方法 設備地址 從芯片手冊上可以知道&#xff0c;AT24C02 的設備地址跟它的 A2、A1、A0 引腳有關&#xff1a; 圖 10.36 AT24C02 設備地址引腳配置 294 / 577 打開 I2C 模塊的原理圖&#xff1a; 開發板配套網盤資料\04_開發板原理圖\ 04_Extend_modules\通…

SQL語句執行過程

一條 SQL 的執行過程可以大致分為以下幾個步驟&#xff1a; 連接器&#xff1a; ○ 客戶端與數據庫建立連接&#xff0c;并發送 SQL 語句給數據庫服務。 ○ 連接器驗證客戶端的身份和權限&#xff0c;確保用戶有足夠的權限執行該 SQL 語句。查詢緩存&#xff1a; ○ 連接器首先…

基于鷹棲息算法優化概率神經網絡PNN的分類預測 - 附代碼

基于鷹棲息算法優化概率神經網絡PNN的分類預測 - 附代碼 文章目錄 基于鷹棲息算法優化概率神經網絡PNN的分類預測 - 附代碼1.PNN網絡概述2.變壓器故障診街系統相關背景2.1 模型建立 3.基于鷹棲息優化的PNN網絡5.測試結果6.參考文獻7.Matlab代碼 摘要&#xff1a;針對PNN神經網絡…

Motion v5.6.7 蘋果電腦上的視頻編輯

Motion mac是一款運行在蘋果電腦上的視頻編輯軟件&#xff0c;它能讓您自定Final Cut Pro字幕、轉場和效果。 它可以在2D或3D空間中創建您自己的精美炫目的動畫&#xff0c;同時還能在您工作時提供實時反饋。廣色域支持讓你的動態圖形更顯出色光彩。3D文字功能經過優化增強&am…

01背包與完全背包學習總結

背包問題分類見下圖 參考學習點擊&#xff1a;代碼隨想錄01背包講解 01背包問題&#xff1a; 核心思路&#xff1a; 1、先遍歷物品個數&#xff0c;再遍歷背包容量。因為容量最先是最大的&#xff0c;往背包里放物品&#xff0c;所以背包容量在慢慢減少&#xff0c;但背包容量…

CentOS7 firewall使用(開放和禁止端口、端口轉發)

安裝 安裝命令 yum install firewalld -y 使用命令 systemctl start firewalld ##開啟防火墻systemctl stop firewalld ##關閉防火墻systemctl status firewalld ##查看防火墻狀態firewall-cmd --reload ##重啟防火墻systemctl enable firewalld ##設置開啟啟動systemctl …

共享內存原理介紹及簡單使用

每當我們執行一個程序時&#xff0c;對于操作系統來講就創建了一個進程,在這個過程中&#xff0c;伴隨著資源的分配和釋放。可以認為進程是一個程序的一次執行過程。進程的內存空間是相互獨立的&#xff0c;一般而言是不能相互訪問的。但很多情況下進程間需要互相通信&#xff…