Java 中Class文件詳解

轉自:JVM中class文件探索與解析

 

(1)首先,編寫簡單代碼,對其編譯生成的class文件進行研究,其java代碼如下: 

SosoImgView Code 

編譯之後,用WinHex軟件打開其class文件,可以看到其編譯的十六進制文件如下: 

SosoImg

  按照上圖分析,開頭的前4個字節 SosoImg,是魔數(類似於拼音“咖啡寶貝”),它的用處是標識該文件是否能被java虛擬機識別; 

  緊接着魔數的4個字節 SosoImg,前兩字節0x00代表次版本號(小數點之後的數字),後兩字節0x0033代表是class文件的主版本號,換算成十進制是51,標識是JDK1.7可識別的版本(不同的版本可以查看class文件版本號表如下:) 

  在主版本號字節之後的是常量池,可以理解爲Class文件的資源倉庫,存儲着與class文件相關的數據項。由於不同class文件,常量池數量不同,常量池入口放置兩個字節的數據 SosoImg(0x0028)爲常量池計數器。十六進制的0x0028爲十進制的40(地址偏移量),代表常量池中有39個常量,索引範圍爲1-40(注:java僅限於class文件結構中容量計數器是從1開始的,java的設計者將索引0拿出來是有特殊考慮的,用來表示不引用任何一個常量池中的項)。 

  常量池中,存放兩類數據:(1)字面量:可以理解爲java中的常量,例如:字符串、final修飾常量等。

  (2)符號引用:主要包括①類、接口的全限定名②字段的名稱和描述符③方法的名稱和描述符

  在常量池裏,存儲常量結構如下:u1(常量標誌位,用於指明常量的類型,可以查看如下常量池項目類型對應表)+常量信息

SosoImg

  讓我們以上述class文件爲例,索引爲1的常量標誌位是0x0A(十進制爲10),對應上表中的CONSTANT_Methoddef_info類型的常量,參考常量結構表如下圖(在jdk1.7中新增了tag=15/16/18的常量類型,更好的支持動態語言的調用,此處就不列舉了),

SosoImg

  該class文件中,常量池裏索引爲1的常量(const#1),項目類型標識符爲0x0A,二進制爲10,查詢上表,代表着類方法的符號引用。緊接着兩個u2字符代表該常量的信息內容,其中方法描述符0x0006爲#6常量,名稱及類型描述爲0x001A指向#26常量。

  緊跟其後的是索引爲2的常量(const#2),其標誌符爲0x09(十進制爲9),是字段的符號引用,緊接着的兩個u2字符代表其引用索引ID,方法的類描述符指向#27常量,字段描述符指向#28常量;

  分析了以上兩個字節之後,這裏就不一一分析後面的常量了,有興趣的可以自己分析下。其他的常量池用jdk自帶的javap進行生成,在windows中打開cmd(安裝jdk並配置環境變量),輸入:javap -verbose class文件路徑,可以看到編譯之後的常量池如下:

SosoImg

  將上述class文件中常量池部分標記圖如下,紅色框代表一個常量池中的項,依次編號爲1-39,

SosoImg

  我們將上圖和javap生成的常量內容對比一下,以const#9爲例,#9項爲:0x01 (utf8類型) 0x0006(佔用字節) 0x3C 0x69 0x6E 0x69 0x74 0x3E(項內容),我們對項內容進行在線轉換,將十六進制轉換ASCII碼值,得到該常量表示:<init>,如下圖:

SosoImg


  與javap生成的常量文件對比,發現兩者完全一致。對字節碼有興趣的朋友可以逐個試一試。

SosoImg

  在常量池區域結束之後,緊接着的一個u2(兩個字節)類型的字符代表訪問標誌,它用於識別類或者接口的訪問信息,例如:class是類還是接口,訪問是private還是public等。訪問標誌表如下圖: 

SosoImg

  在上述文件中,訪問標誌爲: SosoImg,即:0x0021,對照上表,只有ACC_PUBLIC和ACC_SUPER爲真,其他幾項爲假。該類爲public 能夠使用invoke指令。 

  跟在訪問標誌之後的分別是類索引、父類索引 SosoImg 。由於java不允許多繼承,所以類索引和父類索引是一個u2類型的數據。在上述文件中,類索引爲#5常量(TestClass),父類索引爲#6常量(java/lang/Object); 

  緊接着類索引和父類索引的是接口索引信息。在java中一個類可以實現多個接口,所以用u2類型的數據集合來表示接口索引。在接口索引的入口,有一項u2類型的接口計數器 SosoImg,計數器爲0表示接口的索引表不佔用任何字節。 

  在接口相關描述信息之後的,是字段表集合,用於描述類或者接口中申明的變量(注:此處的變量是指類或者接口級變量,即類變量或者實例級變量,而不包括方法中的局部變量)。這些字段通常包含哪些信息呢?通常有:字段訪問域(private、public、protected等)+是實例變量還是類變量(Static)+是否可修改(final)+併發可見性(volatitle,用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最終的值)+可否序列化(transient)+字段類型(基本類型、對象、數組)+字段名稱。在JVM中,字段表結構如下: 

  我們來看,字段表集合的入口u2類型數據項是字段表的容量計數器爲 SosoImg(0x0001),表示只有一個字段項。緊接着字段表容量計數器的u2類型的數據爲0x0002,參考下表,表示該字段爲private。字段名稱 SosoImg爲0x0007,其值爲“m”,描述信息0x0008,其值爲“I”,可以推斷,原代碼的定義字段爲:“private int m”; 

標誌名稱 標誌值 含義  
ACC_PUBLIC 0x00 01 字段是否爲public  
ACC_PRIVATE 0x00 02 字段是否爲private  
ACC_PROTECTED 0x00 04 字段是否爲protected  
ACC_STATIC 0x00 08 字段是否爲static  
ACC_FINAL 0x00 10 字段是否爲final  
ACC_VOLATILE 0x00 40 字段是否爲volatile  
ACC_TRANSTENT 0x00 80 字段是否爲transient  
ACC_SYNCHETIC 0x10 00 字段是否爲由編譯器自動產生  
ACC_ENUM 0x40 00 字段是否爲enum  

  通常而言,在字段描述之後還有一些屬性表信息存儲額外的信息,在以上class文件中,屬性計數器位0x0000,表示沒有額外的屬性信息。

  在字段表之後的是方法表集合,其表示方法與字段信息表幾乎一致。其結構如下表: 

  但是,方法表的修飾屬性比字段表要多,例如:方法有abstract、synchronize等。在方法表的入口,同樣也有一個方法容量計數器,佔用u2字節。在上述class中,方法的計數器爲 SosoImg,即0x0003表示有三個方法。由上圖可以看出,方法的前4個u2位分別爲:訪問標誌、方法名索引、描述索引和屬性數量。接在屬性數量之後的,即爲屬性表集合。 

  我們來分析一下,第一個方法,function#1的第一、二、三、四u2數據項分別爲:0x0001、0x0009、0x000A、0x0001,代表方法爲public、方法名指向const#9(”<init>”)、方法描述爲const#10(“()V”)、含有一個屬性。然後我們看一下屬性表,

屬性表的頭兩位爲屬性名稱索引,即0x000B(二進制11)該屬性指向const#11(“Code”)屬性,java呈現方法體重的代碼經過javac編譯之後,就存儲在Code屬性裏。 


SosoImg

  (根據上表結構)接着的4個u2類型爲屬性長度,即 SosoImg,(換算成二進制47),表示屬性長度爲47。下圖標紅部分,即爲該方法中屬性的長度 

。   SosoImg

  表示屬性即爲之後的47個u1字節,

在屬性長度之後的,兩個u2類型的項分別是:操作數棧的最大深度局部變量表存儲的空間(最小單位爲Slot)。對應class文件中的均爲:0x0001,表示操作數棧的最大深度和局部變量表存儲的空間均爲1。之後,有一個u4類型的項0x00000005(二進制爲5),表示字節碼長度爲5。在字節碼長度之後緊跟的便是字節碼,該項的長度與字節碼的長度(code_length)完全一致,上述class中字節碼長度爲5個u2類型的項,對應0x2A 0xB7 0x00 0x01 0xB1,這裏解釋下以上字節碼的含義。(關於字節碼指令可以查看字節碼指令表,由於項太多就不一一列舉了)。 

  0x2A——對應指令aload_0,意思是將第0個Slot中爲reference類型的本地變量推送到操作數棧頂。

  0xB7——對應指令inbokespecial,意思是將以棧頂的reference類型的數據所指向的對象作爲接收者,調用該類型的實例構造方法。

  0x00 01——對應指令invokespecial的參數,差的常量池0x0001對應的const#1對應的<init>方法的符號引用。

  0xB1——指令爲return,意識是返回此方法,並且返回值爲void;當次行命令執行之後,方法執行結束。

  在方法字節碼之後,便是異常表exceptions_table。在異常表的入口,有一項u2類型的數據,代表異常表的長度,對應上述class文件中的0x0000 ,表示該Code中沒有異常表, 

  接在Code的異常表之後的u2項表示屬性表,上述class中爲0x0002(換算成十進制=2),表示該方法的Code屬性中有兩項Code屬性,它是class文件中最重要的一個屬性。如果把java代碼分爲方法體中的代碼和元數據兩部分,Code屬性用於秒速代碼,所有其他屬性用於描述元數據。我們來看第一個Code屬性,入口u2項爲0x000C(換算成十進制的12),我們查看常量池中const#12代表LineNumberTable。這個屬性是用於描述java源碼和字節碼行號之間的對應關係。若沒有改項,程序編譯不會出錯,但是在拋出異常時,無法知道異常所在的行號。接着分析,我們來看一下LineNumberTable屬性的結構: 

SosoImg根據上表,我們看一下接下來的u4項爲:0x00000006,表示該屬性的長度爲6,即0x00 0x01 0x00 0x00 0x00 0x04,接下來的u2項0x0001表示有一個類型爲line_number_info的集合。line_number_info爲startpx和line_number兩個u2類型的項,前者表示字節碼起始行號,後者表示java行號,一一對應。分別對應class文件中的0x0000 和0x0004,表示字節碼中0行和java代碼中4行對應。(PS前三行爲註釋,字節碼不翻譯,所以對應關係是正確的),所裏這裏便是Code的第一個屬性——LineNumberTable屬性。 

在之前的分析中,Code應該包含兩個屬性。我們接着來看看Code的第二個屬性,我們看下第二個屬性的入口u2項,0x000D(對應十進制的13),還是跟之前一樣,查詢常量池中const#13對應的項,我們可以看到便是LocalVariableTable屬性。這個屬性作用是什麼呢?它是用於描述棧幀中局部變量表中的變量與java源碼中定義變量之間的關係的。我們來看一下它的屬性結構,如下圖: 

SosoImg

  前面的u2和u4項分別代表其屬性對應的索引和屬性字節長度。索引爲13,長度爲0x0000000C,表示字節碼有12個u1項。即爲圖中標出的12項,

SosoImg

  在這12項中,頭u2項爲0x0001,表示local_variable_info_table的長度爲1,我們看一下local_variable_info_table的結構,如下圖

SosoImg

  其中,start_pc和length分別代表了這個局部變量的生命週期字節碼起始位置和其覆蓋範圍長度,對應的起始位置字節偏移量和覆蓋長度分別爲0x0000 和0x0005;name_index和descriptor_index都是指向常量池中的索引,代表了這個局部變量名稱和其描述符,對應的是0x000E(14,對應常量池中的this)和0x000F(15,對應常量池中的LTestClass)。index表示這個局部變量在棧幀中的Slot位置,0x0000表示從佔用index=0的Slot位置,這與之前this佔用index=0的理論一致。

  好了,到目前位置,我們已經分析過class文件中的一個方法表了,由於方法表結構複雜,剩下的兩個就不一一分析了,下面我把另外兩個方法所佔用的字節給大家標註一下,大家有興趣的可以自己研究、自行分析下。

SosoImg

上圖中,以垂直分割線分開的即爲一個方法描述,可以自行分析下。通過javap命令生成的以上方法截圖如下: 

SosoImg

接着方法表後面的,即爲class文件SourceFile屬性。其結構如下圖: 

SosoImg

入口0x0001,表示有一個SourceFile屬性,接下來的u2、u4和u2項(0x0018、0x00000002、0x0019)轉換成十進制爲(24,2,25),對應着屬性名索引、屬性長度和sourcefile的索引,即爲SourceFile TestClass.java,完全與java代碼一致。 

自此,關於class的文件解析工作就結束了,文中有不對的地方,歡迎各位大神批評指正。(文中結構表均爲粘貼,如若轉發,請標明出處!) 

發佈了47 篇原創文章 · 獲贊 21 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章