常量池中各數據項類型詳解java
關於常量池的大概內容, 已經在 深刻理解Java Class文件格式(一) 中講解過了, 這篇文章中還介紹了常量池中的11種數據類型。 本文的任務是詳細講解這11種數據類型, 深度剖析源文件中的各類信息是以什麼方式存放在常量池中的。 工具
咱們知道, 常量池中的數據項是經過索引來引用的, 常量池中的各個數據項之間也會相互引用。在這11中常量池數據項類型中, 有兩種比較基礎, 之因此說它們基礎, 是由於這兩種類型的數據項會被其餘類型的數據項引用。 這兩種數據類型就是CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中CONSTANT_NameAndType類型的數據項(CONSTANT_NameAndType_info)也會引用CONSTANT_Utf8類型的數據項(CONSTANT_Utf8_info) 。 與其餘介紹常量池的書籍或其餘資料不一樣, 本着按部就班和前後分明的原則, 咱們首先對這兩種比較基本的類型的數據項進行介紹, 而後再依次介紹其餘9中數據項。 佈局
(1) CONSTANT_Utf8_infothis
一個CONSTANT_Utf8_info是一個CONSTANT_Utf8類型的常量池數據項, 它存儲的是一個常量字符串。 常量池中的全部字面量幾乎都是經過CONSTANT_Utf8_info描述的。下面咱們首先講解CONSTANT_Utf8_info數據項的存儲格式。在前面的文章中, 咱們提到, 常量池中數據項的類型由一個整型的標誌值(tag)決定, 因此全部常量池類型的info中都必須有一個tag信息, 而且這個tag值位於數據項的第一個字節上。 一個11中常量池數據類型, 因此就有11個tag值表示這11中類型。而CONSTANT_Utf8_info的tag值爲1, 也就是說若是虛擬機要解析一個常量池數據項, 首先去讀這個數據項的第一個字節的tag值, 若是這個tag值爲1, 那麼就說明這個數據項是一個CONSTANT_Utf8類型的數據項。 緊挨着tag值的兩個字節是存儲的字符串的長度length, 剩下的字節就存儲着字符串。 因此, 它的格式是這樣的:spa
其中tag佔一個字節, length佔2個字節, bytes表明存儲的字符串, 佔length字節。因此, 若是這個CONSTANT_Utf8_info存儲的是字符串"Hello", 那麼他的存儲形式是這樣的:3d
如今咱們知道了CONSTANT_Utf8_info數據項的存儲形式, 那麼CONSTANT_Utf8_info數據項都存儲了什麼字符串呢? CONSTANT_Utf8_info可包括的字符串主要如下這些:
程序中的字符串常量
常量池所在當前類(包括接口和枚舉)的全限定名
常量池所在當前類的直接父類的全限定名
常量池所在當前類型所實現或繼承的全部接口的全限定名
常量池所在當前類型中所定義的字段的名稱和描述符
常量池所在當前類型中所定義的方法的名稱和描述符
由當前類所引用的類型的全限定名
由當前類所引用的其餘類中的字段的名稱和描述符
由當前類所引用的其餘類中的方法的名稱和描述符
與當前class文件中的屬性相關的字符串, 如屬性名等
總結一下, 其中有這麼五類: 程序中的字符串常量, 類型的全限定名, 方法和字段的名稱, 方法和字段的描述符, 屬性相關字符串。 程序中的字符串常量不用多說了, 咱們常用它們建立字符串對象, 屬性相關的字符串, 等到講到class中的屬性信息(attibute)時自會說起。 方法和字段的名稱也不用多說了 。 剩下的就是類型的全限定名,方法和字段的描述符, 這就是上篇文章中說起的"特殊字符串", 不熟悉的同窗能夠先讀一下上篇文章 深刻理解Java Class文件格式(二) 。 還有一點須要說明, 類型的全限定名, 方法和字段的名稱, 方法和字段的描述符, 能夠是本類型中定義的, 也多是本類中引用的其餘類的。
下面咱們經過一個例子來進行說明。 示例源碼:code
package com.jg.zhang; public class Programer extends Person { static String company = "CompanyA"; static{ System.out.println("staitc init"); } String position; Computer computer; public Programer() { this.position = "engineer"; this.computer = new Computer(); } public void working(){ System.out.println("coding..."); computer.working(); } }
別看這個類簡單, 可是反編譯後, 它的常量池有53項之多。 在這53項常量池數據項中, 各類類型的數據項都有, 固然也包括很多的CONSTANT_Utf8_info 。 下面只列出反編譯後常量池中的CONSTANT_Utf8_info 數據項:
#2 = Utf8 com/jg/zhang/Programer //當前類的全限定名
#4 = Utf8 com/jg/zhang/Person //父類的全限定名
#5 = Utf8 company //company字段的名稱
#6 = Utf8 Ljava/lang/String; //company和position字段的描述符
#7 = Utf8 position //position字段的名稱
#8 = Utf8 computer //computer字段的名稱
#9 = Utf8 Lcom/jg/zhang/Computer; //computer字段的描述符
#10 = Utf8 <clinit> //類初始化方法(即靜態初始化塊)的方法名
#11 = Utf8 ()V //working方法的描述符
#12 = Utf8 Code //Code屬性的屬性名
#14 = Utf8 CompanyA //程序中的常量字符串
#19 = Utf8 java/lang/System //所引用的System類的全限定名
#21 = Utf8 out //所引用的out字段的字段名
#22 = Utf8 Ljava/io/PrintStream; //所引用的out字段的描述符
#24 = Utf8 staitc init //程序中的常量字符串
#27 = Utf8 java/io/PrintStream //所引用的PrintStream類的全限定名
#29 = Utf8 println //所引用的println方法的方法名
#30 = Utf8 (Ljava/lang/String;)V //所引用的println方法的描述符
#31 = Utf8 LineNumberTable //LineNumberTable屬性的屬性名
#32 = Utf8 LocalVariableTable //LocalVariableTable屬性的屬性名
#33 = Utf8 <init> //當前類的構造方法的方法名
#41 = Utf8 com/jg/zhang/Computer //所引用的Computer類的全限定名
#45 = Utf8 this //局部變量this的變量名
#46 = Utf8 Lcom/jg/zhang/Programer; //局部變量this的描述符
#47 = Utf8 working //woking方法的方法名
#49 = Utf8 coding... //程序中的字符串常量
#52 = Utf8 SourceFile //SourceFile屬性的屬性名
#53 = Utf8 Programer.java //當前類所在的源文件的文件名對象
上面只列出了反編譯結果中常量池中的CONSTANT_Utf8_info數據項。 其中第三列不是javap反編譯的輸出結果, 而是我加上的註釋。 讀者能夠對比上面的程序源碼來看一下, 這樣的話, 就能夠清楚的看出, 源文件中的各類字符串, 是如何和存放到CONSTANT_Utf8_info中的。blog
這裏要強調一下, 源文件中的幾乎全部可見的字符串都存放在CONSTANT_Utf8_info中, 其餘類型的常量池項只不過是對CONSTANT_Utf8_info的引用。 其餘常量池項, 把引用的CONSTANT_Utf8_info組合起來, 進而能夠描述更多的信息。 下面將要介紹的CONSTANT_NameAndType_info就能夠驗證這個結論。繼承
(2) CONSTANT_NameAndType類型的數據項
常量池中的一個CONSTANT_NameAndType_info數據項, 能夠看作CONSTANT_NameAndType類型的一個實例 。 從這個數據項的名稱能夠看出, 它描述了兩種信息,第一種信息是名稱(Name), 第二種信息是類型(Type) 。 這裏的名稱是指方法的名稱或者字段的名稱, 而Type是廣義上的類型, 它其實描述的是字段的描述符或方法的描述符。 也就是說, 若是Name部分是一個字段名稱, 那麼Type部分就是相應字段的描述符; 若是Name部分描述的是一個方法的名稱, 那麼Type部分就是對應的方法的描述符。 也就是說, 一個CONSTANT_NameAndType_info就表示了一個方法或一個字段。
下面先看一下CONSTANT_NameAndType_info數據項的存儲格式。 既然是常量池中的一種數據項類型, 那麼它的第一個字節也是tag, 它的tag值是12, 也就是說, 當虛擬機讀到一個tag爲12的常量池數據項, 就能夠肯定這個數據項是一個CONSTANT_NameAndType_info 。 tag值一下的兩個字節叫作name_index, 它指向常量池中的一個CONSTANT_Utf8_info, 這個CONSTANT_Utf8_info中存儲的就是方法或字段的名稱。 name_index之後的兩個字節叫作descriptor_index, 它指向常量池中的一個CONSTANT_Utf8_info, 這個CONSTANT_Utf8_info中存儲的就是方法或字段的描述符。 下圖表示它的存儲佈局:
下面舉一個實例進行說明, 實例的源碼爲:
package com.jg.zhang; public class Person { int age; int getAge(){ return age; } }
這個Person類很簡單, 只有一個字段age, 和一個方法getAge 。 將這段代碼使用javap工具反編譯以後, 常量池信息以下:
#1 = Class #2 // com/jg/zhang/Person
#2 = Utf8 com/jg/zhang/Person
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 age
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #7:#8 // "<init>":()V
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/jg/zhang/Person;
#16 = Utf8 getAge
#17 = Utf8 ()I
#18 = Fieldref #1.#19 // com/jg/zhang/Person.age:I
#19 = NameAndType #5:#6 // age:I
#20 = Utf8 SourceFile
#21 = Utf8 Person.java
常量池一共有21項, 咱們能夠看到, 一共有兩個CONSTANT_NameAndType_info 數據項, 分別是第#11項和第#19項, 其中第#11項的CONSTANT_NameAndType_info又引用了常量池中的第#7項和第#8項, 被引用的這兩項都是CONSTANT_Utf8_info , 它們中存儲的字符串常量值分別是 <init> 和 ()V。 其實他們加起來表示的就是父類Object的構造方法。 那麼這裏爲何會是父類Object的構造方法而不是本類的構造方法呢? 這是由於類中定義的方法若是不被引用(也就是說在當前類中不被調用), 那麼常量池中是不會有相應的 CONSTANT_NameAndType_info 與之對應的, 只有引用了一個方法, 纔有相應的CONSTANT_NameAndType_info 與之對應。 這也是爲何說CONSTANT_NameAndType_info 是方法的符號引用的一部分的緣由。 (這裏提到一個新的概念, 叫作方法的符號引用, 這個概念會在後面的博客中進行講解) 能夠看到, 在源碼存在兩個方法, 分別是編譯器默認添加的構造方法和咱們本身定義的getAge方法, 由於並無在源碼中顯示的調用這兩個方法,因此在常量池中並不存在和這兩個方法相對應的CONSTANT_NameAndType_info 。 之因此會存在父類Object的構造方法對應的CONSTANT_NameAndType_info , 是由於子類構造方法中會默認調用父類的無參數構造方法。 咱們將常量中的其餘信息去掉, 能夠看得更直觀:
下面講解常量池第#19項的CONSTANT_NameAndType_info , 它引用了常量池第#5項和第#6項, 這兩項也是CONSTANT_Utf8_info 項, 其中存儲的字符串分別是age和I, 其中age是源碼中字段age的字段名, I是age字段的描述符。 因此這個CONSTANT_NameAndType_info 就表示對本類中的字段age的引用。 除去常量池中的其餘信息, 能夠看得更直觀:
和方法相同, 只定義一個字段而不引用它(在源碼中表現爲不訪問這個變量), 那麼在常量池中也不會存在和該字段相對應的CONSTANT_NameAndType_info 項。這也是爲何說CONSTANT_NameAndType_info做爲字段符號引用的一部分的緣由。 (這裏提到一個新的概念, 叫作字段的符號引用, 這個概念會在後面的博客中進行講解) 在本例中之因此會出現這個CONSTANT_NameAndType_info , 是由於在源碼的getAge方法中訪問了這個字段:
int getAge(){ return age; }
下面給出這兩個CONSTANT_NameAndType_info真實的內存佈局圖:
和Object構造方法相關的CONSTANT_NameAndType_info的示意圖:
和age字段相關的CONSTANT_NameAndType_info示意圖:
這兩張圖可以很好的反映出CONSTANT_NameAndType_info和CONSTANT_Utf8_info 這兩種常量池數據項的數據存儲方式, 也可以真實的反應CONSTANT_NameAndType_info和CONSTANT_Utf8_info 的引用關係。
總結
本篇博客就到此爲止, 在本文中咱們主要介紹了常量池中的兩種數據項: CONSTANT_NameAndType_info 和 CONSTANT_Utf8_info 。 其中CONSTANT_Utf8_info存儲的是源文件中的各類字符串, 而CONSTANT_NameAndType_info表述的是源文件中對一個字段或方法的符號引用的一部分(即 方法名加方法描述符, 或者是 字段名加字段描述符)。在下一篇博客中, 繼續講解常量池中的其餘類型的數據項 。