JVM中class文件探索與解析(一)
一直想成為一名優秀的架構師的我,轉眼已經工作快兩年了,對於java內核了解甚少,閑來時間,看看JVM,吧自己的一些研究寫下來供大家參考,有不對的地方請指正。
廢話不多說,一起來看看JVM中類文件是如何加載和運行的。
(1)首先,編寫簡單代碼,對其編譯生成的class文件進行研究,其java代碼如下:
1 public class test { 2 private static int count = 0; 3 public static void recursion(){ 4 count++; 5 recursion(); 6 }View Code7 8 public static void main (String args[]){ 9 try { 10 recursion(); 11 }catch (Exception ex){ 12 System.out.print("deep of callings:"+count+"\n"); 13 ex.printStackTrace(); 14 } 15 } 16 }
編譯之後,用WinHex軟件打開其class文件,可以看到其編譯的十六進制文件如下:
按照上圖分析,開頭的前4個字節,是魔數(類似於拼音“咖啡寶貝”),它的用處是標識該文件是否能被java虛擬機識別;
緊接著魔數的4個字節,前兩字節0x00代表次版本號(小數點之後的數字),後兩字節0x0033代表是class文件的主版本號,換算成十進制是51,標識是JDK1.7可識別的版本(不同的版本可以查看class文件版本號表如下:)
版本號 | 對應十進制 | jdk版本號 |
---|---|---|
2E | 46 | jdk1.2 |
2F | 47 | jdk1.3 |
30 | 48 | jdk1.4 |
31 | 49 | jdk1.5 |
32 | 50 | jdk1.6 |
33 | 51 | jdk1.7 |
34 | 52 | jdk1.8 |
在主版本號字節之後的是常量池,可以理解為Class文件的資源倉庫,存儲著與class文件相關的數據項。由於不同class文件,常量池數量不同,常量池入口放置兩個字節的數據(0x0028)為常量池計數器。十六進制的0x0028為十進制的40(地址偏移量),代表常量池中有39個常量,索引範圍為1-40(註:java僅限於class文件結構中容量計數器是從1開始的,java的設計者將索引0拿出來是有特殊考慮的,用來表示不引用任何一個常量池中的項)。
常量池中,存放兩類數據:(1)字面量:可以理解為java中的常量,例如:字符串、final修飾常量等。
(2)符號引用:主要包括①類、接口的全限定名②字段的名稱和描述符③方法的名稱和描述符
在常量池裏,存儲常量結構如下:u1(常量標誌位,用於指明常量的類型,可以查看如下常量池項目類型對應表)+常量信息
讓我們以上述class文件為例,索引為1的常量標誌位是0x0A(十進制為10),對應上表中的CONSTANT_Methoddef_info類型的常量,參考常量結構表如下圖(在jdk1.7中新增了tag=15/16/18的常量類型,更好的支持動態語言的調用,此處就不列舉了),
該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文件路徑,可以看到編譯之後的常量池如下:
將上述class文件中常量池部分標記圖如下,紅色框代表一個常量池中的項,依次編號為1-39,
我們將上圖和javap生成的常量內容對比一下,以const#9為例,#9項為:0x01 (utf8類型) 0x0006(占用字節) 0x3C 0x69 0x6E 0x69 0x74 0x3E(項內容),我們對項內容進行在線轉換,將十六進制轉換ASCII碼值,得到該常量表示:<init>,如下圖:
與javap生成的常量文件對比,發現兩者完全一致。對字節碼有興趣的朋友可以逐個試一試。
在常量池區域結束之後,緊接著的一個u2(兩個字節)類型的字符代表訪問標誌,它用於識別類或者接口的訪問信息,例如:class是類還是接口,訪問是private還是public等。訪問標誌表如下圖:
在上述文件中,訪問標誌為:,即:0x0021,對照上表,只有ACC_PUBLIC和ACC_SUPER為真,其他幾項為假。該類為public 能夠使用invoke指令。
跟在訪問標誌之後的分別是類索引、父類索引。由於java不允許多繼承,所以類索引和父類索引是一個u2類型的數據。在上述文件中,類索引為#5常量(TestClass),父類索引為#6常量(java/lang/Object);
緊接著類索引和父類索引的是接口索引信息。在java中一個類可以實現多個接口,所以用u2類型的數據集合來表示接口索引。在接口索引的入口,有一項u2類型的接口計數器,計數器為0表示接口的索引表不占用任何字節。
在接口相關描述信息之後的,是字段表集合,用於描述類或者接口中申明的變量(註:此處的變量是指類或者接口級變量,即類變量或者實例級變量,而不包括方法中的局部變量)。這些字段通常包含哪些信息呢?通常有:字段訪問域(private、public、protected等)+是實例變量還是類變量(Static)+是否可修改(final)+並發可見性(volatitle,用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最終的值)+可否序列化(transient)+字段類型(基本類型、對象、數組)+字段名稱。在JVM中,字段表結構如下:
類型 | 名稱 | 數量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
我們來看,字段表集合的入口u2類型數據項是字段表的容量計數器為(0x0001),表示只有一個字段項。緊接著字段表容量計數器的u2類型的數據為0x0002,參考下表,表示該字段為private。字段名稱為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,表示沒有額外的屬性信息。
在字段表之後的是方法表集合,其表示方法與字段信息表幾乎一致。其結構如下表:
類型 | 名稱 | 數量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
但是,方法表的修飾屬性比字段表要多,例如:方法有abstract、synchronize等。在方法表的入口,同樣也有一個方法容量計數器,占用u2字節。在上述class中,方法的計數器為,即0x0003表示有三個方法。
第一個方法,function#1的第一、二、三、四、五項u2數據項分別為:0x0001、0x0009、0x000A、0x0001、0x000B,代表方法為public、方法名指向const#9("<init>")、方法描述為const#10(“()V”)、含有一個屬性、該屬性指向const#11(“Code”)屬性,java呈現方法體重的代碼經過javac編譯之後,就存儲在Code屬性裏。
(ps:今天的class文件探索就寫到這裏,過幾天接著寫...休息一下)
JVM中class文件探索與解析(一)