读《深入jvm原理》之class文件
較為深入的學習者,可能會給出跨語言編程的概念和實現,class文件就是一個跨語言的實現的第一步,也是動態修改整理已完成編碼的代碼,去生成新代碼的指令的劃時代編程的第一步(動態類生成技術)。
動態生成類技術:
class文件中的信息是一項一項排列的, 每項數據都有它的固定長度, 有的占一個字節, 有的占兩個字節, 還有的占四個字節或8個字節, 數據項的不同長度分別用u1, u2, u4, u8表示, 分別表示一種數據項在class文件中占據一個字節, 兩個字節, 4個字節和8個字節。 可以把u1, u2, u3, u4看做class文件數據項的“類型” 。
class文件中存在以下數據項(該圖表參考自《深入Java虛擬機》):
| 類型 | 名稱 | 數量 |
| u4 | magic | 1 |
| u2 | minor_version | 1 |
| u2 | major_version | 1 |
| u2 | constant_pool_count | 1 |
| cp_info | constant_pool | constant_pool_count - 1 |
| u2 | access_flags | 1 |
| u2 | this_class | 1 |
| u2 | super_class | 1 |
| u2 | interfaces_count | 1 |
| u2 | interfaces | interfaces_count |
| u2 | fields_count | 1 |
| field_info | fields | fields_count |
| u2 | methods_count | 1 |
| method_info | methods | methods_count |
| u2 | attribute_count | 1 |
| attribute_info | attributes | attributes_count |
下面對class文件中的每一項進行詳細的解釋。fields(域,字段或者信息組,包含方法和屬性)
class文件中的魔數和版本號
(1) magic
在class文件開頭的四個字節, 存放著class文件的魔數, 這個魔數是class文件的標志,他是一個固定的值: 0XCAFEBABE 。 也就是說他是判斷一個文件是不是class格式的文件的標準, 如果開頭四個字節不是0XCAFEBABE, 那么就說明它不是class文件, 不能被JVM識別。
(2)minor_version 和 major_version
緊接著魔數的四個字節是class文件的此版本號和主版本號。 隨著Java的發展, class文件的格式也會做相應的變動。 版本號標志著class文件在什么時候, 加入或改變了哪些特性。 舉例來說, 不同版本的javac編譯器編譯的class文件, 版本號可能不同, 而不同版本的JVM能識別的class文件的版本號也可能不同, 一般情況下, 高版本的JVM能識別低版本的javac編譯器編譯的class文件, 而低版本的JVM不能識別高版本的javac編譯器編譯的class文件。 如果使用低版本的JVM執行高版本的class文件,?JVM會拋出java.lang.UnsupportedClassVersionError 。具體的版本號變遷這里不再討論, 需要的讀者自行查閱資料。?
class文件中的常量池概述
在class文件中, 位于版本號后面的就是常量池相關的數據項。 常量池是class文件中的一項非常重要的數據。 常量池中存放了文字字符串, 常量值, 當前類的類名, 字段名, 方法名, 各個字段和方法的描述符, 對當前類的字段和方法的引用信息, 當前類中對其他類的引用信息等等。?常量池中幾乎包含類中的所有信息的描述, class文件中的很多其他部分都是對常量池中的數據項的引用,比如后面要講到的this_class, super_class, field_info, attribute_info等, 另外字節碼指令中也存在對常量池的引用, 這個對常量池的引用當做字節碼指令的一個操作數。 ?此外, 常量池中各個項也會相互引用。
class文件中的項constant_pool_count的值為1, 說明每個類都只有一個常量池。 常量池中的數據也是一項一項的, 沒有間隙的依次排放。常量池中各個數據項通過索引來訪問, 有點類似與數組, 只不過常量池中的第一項的索引為1, 而不為0, 如果class文件中的其他地方引用了索引為0的常量池項, 就說明它不引用任何常量池項。class文件中的每一種數據項都有自己的類型, 相同的道理,常量池中的每一種數據項也有自己的類型。 常量池中的數據項的類型如下表:
| 常量池中數據項類型 | 類型標志 | 類型描述 |
| CONSTANT_Utf8 | 1 | UTF-8編碼的Unicode字符串 |
| CONSTANT_Integer | 3 | int類型字面值 |
| CONSTANT_Float | 4 | float類型字面值 |
| CONSTANT_Long | 5 | long類型字面值 |
| CONSTANT_Double | 6 | double類型字面值 |
| CONSTANT_Class | 7 | 對一個類或接口的符號引用 |
| CONSTANT_String | 8 | String類型字面值 |
| CONSTANT_Fieldref | 9 | 對一個字段的符號引用 |
| CONSTANT_Methodref | 10 | 對一個類中聲明的方法的符號引用 |
| CONSTANT_InterfaceMethodref | 11 | 對一個接口中聲明的方法的符號引用 |
| CONSTANT_NameAndType | 12 | 對一個字段或方法的部分符號引用 |
每個數據項叫做一個XXX_info項, 比如, 一個常量池中一個CONSTANT_Utf8類型的項, 就是一個CONSTANT_Utf8_info 。除此之外, 每個info項中都有一個標志值(tag), 這個標志值表明了這個常量池中的info項的類型是什么, 從上面的表格中可以看出, 一個CONSTANT_Utf8_info中的tag值為1, 而一個CONSTANT_Fieldref_info中的tag值為9 。
Java程序是動態鏈接的, 在動態鏈接的實現中, 常量池扮演者舉足輕重的角色。 除了存放一些字面量之外, 常量池中還存放著以下幾種符號引用:
(1) 類和接口的全限定名
(2) 字段的名稱和描述符
(3) 方法的名稱和描述符
在詳細講解常量池中的各個數據項之前, 我們有必要先了解一下class文件中的特殊字符串, 因為在常量池中, 特殊字符串大量的出現,這些特殊字符串就是上面說的全限定名和描述符。 要理解常量池中的各個數據項, 必須先了解這些特殊字符串。
class文件中的特殊字符串
首先說明一下, 所謂的特殊字符串出現在class文件中的常量池中, 所以在上一篇博客中, 只是對常量池介紹了一個大概。 本著循序漸進和減少跨度的原則, 首先把class文件中的特殊字符串做一個詳細的介紹, 然后再回過頭來繼續講解常量池。?
在上文中, 我們提到特殊字符串是常量池中符號引用的一部分, 至于符號引用的概念, 會在以后提到。 現在我們將重點放在特殊字符串上。 特殊字符串包括三種: 類的全限定名, 字段和方法的描述符, 特殊方法的方法名。 下面我們就分別介紹這三種特殊字符串。
(1) 類的全限定名
在常量池中, 一個類型的名字并不是我們在源文件中看到的那樣, 也不是我們在源文件中使用的包名加類名的形式。 源文件中的全限定名和class文件中的全限定名不是相同的概念。 源文件中的全新定名是包名加類名, 包名的各個部分之間,包名和類名之間, 使用點號分割。 如Object類, 在源文件中的全限定名是java.lang.Object 。 而class文件中的全限定名是將點號替換成“/” 。 例如, Object類在class文件中的全限定名是 java/lang/Object 。 如果讀者之前沒有接觸過class文件格式, 是class文件格式的初學者, 在這里不必知道全限定名在class文件中是如何使用的, 只需要知道, 源文件中一個類的名字, 在class文件中是用全限定名表述的。?
(2) 描述符
我們知道在一個類中可以有若干字段和方法, 這些字段和方法在源文件中如何表述, 我們再熟悉不過了。 既然現在我們要學習class文件格式, 那么我們就要問, 一個字段或一個方法在class文件中是如何表述的? 在本文中, 我們會討論方法和字段在class文件中的描述。 方法和字段的描述符并不會把方法和字段的所有信息全都描述出來, 畢竟描述符只是一個簡單的字符串。?
在講解描述符之前, 要先說明一個問題, 那就是所有的類型在描述符中都有對應的字符或字符串來對應。 比如, 每種基本數據類型都有一個大寫字母做對應, void也有一個大寫字符做對應。 下表是void和基本數據類型在描述符中的對應。
| 基本數據類型和void類型 | 類型的對應字符 |
| byte | B |
| char | C |
| double | D |
| float | F |
| int | I |
| long | J |
| short | S |
| boolean | Z |
| void | V |
基本上都是以類型的首字符變成大寫來對應的, 其中long和boolean是特例, long類型在描述符中的對應字符是J, boolean類型在描述符中的對應字符是Z 。?
基本類型和void在描述符中都有一個大寫字符和他們對應, 那么引用類型(類和接口,枚舉)在描述符中是如何對應的呢? 引用類型的對應字符串(注意, 引用類型在描述符中使用一個字符串做對應) , 這個字符串的格式是: “L” + 類型的全限定名 + “;”
注意,這三個部分之間沒有空格, 是緊密排列的。 如Object在描述符中的對應字符串是: Ljava/lang/Object; ?; ArrayList在描述符中的對應字符串是: Ljava/lang/ArrayList; ?; 自定義類型com.example.Person在描述符中的對應字符串是: Lcom/example/Person; 。
我們知道, 在Java語言中數組也是一種類型, 一個數組的元素類型和他的維度決定了他的類型。 比如, 在 int[] a 聲明中, 變量a的類型是int[] , 在 int[][] b 聲明中, 變量b的類型是int[][] , 在 Object[] c 聲明中, 變量c的類型是Object[] 。既然數組是類型, 那么在描述符中, 也應該有數組類型的對應字符串。 在class文件的描述符中, 數組的類型中每個維度都用一個 [ 代表, 數組類型整個類型的對應字符串的格式如下: 若干個“[” + 數組中元素類型的對應字符串
下面舉例來說名。?int[]類型的對應字符串是: [I ;int[][]類型的對應字符串是: [[I ;Object[]類型的對應字符串是: [Ljava/lang/Objec;Object[][][]類型的對應字符串是: [[[Ljava/lang/Object。
介紹完每種類型在描述符中的對應字符串, 下面就開始講解字段和方法的描述符。?
字段的描述符就是字段的類型所對應的字符或字符串。 如: int i 中, 字段i的描述符就是 I ;Object o中, 字段o的描述符就是 Ljava/lang/Object;double[][] d中, 字段d的描述符就是 [[D 。?
方法的描述符比較復雜, 包括所有參數的類型列表和方法返回值。 它的格式是這樣的: (參數1類型 參數2類型 參數3類型 ...)返回值類型 其中, 不管是參數的類型還是返回值類型, 都是使用對應字符和對應字符串來表示的, 并且參數列表使用小括號括起來, 并且各個參數類型之間沒有空格, 參數列表和返回值類型之間也沒有空格。?
下面舉例說明(此表格來源于《深入Java虛擬機》)。
| 方法描述符 | 方法聲明 |
| ()I | int getSize() |
| ()Ljava/lang/String; | String toString() |
| ([Ljava/lang/String;)V | void main(String[] args) |
| ()V | void wait() |
| (JI)V | void wait(long timeout, int nanos) |
| (ZILjava/lang/String;II)Z | boolean regionMatches(boolean ignoreCase, int toOffset, String other, int ooffset, int len) |
| ([BII)I | int read(byte[] b, int off, int len ) |
| ()[[Ljava/lang/Object; | Object[][] getObjectArray() |
?
(3) 特殊方法的方法名
首先要明確一下, 這里的特殊方法是指的類的構造方法和類型初始化方法。 構造方法就不用多說了, 至于類型的初始化方法, 對應到源碼中就是靜態初始化塊。 也就是說, 靜態初始化塊, 在class文件中是以一個方法表述的, 這個方法同樣有方法描述符和方法名。?
類的構造方法的方法名使用字符串 <init> 表示, 而靜態初始化方法的方法名使用字符串?<clinit> 表示。 除了這兩種特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同。
總結
以上是生活随笔為你收集整理的读《深入jvm原理》之class文件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 续航超1000km 极氪001正式交付搭
- 下一篇: Java 动态代理与class字节码动态