常量池計數器
常量池是class文件中非常重要的結構,它描述着整個class文件的字面量信息。 常量池是由一組constant_pool結構體數組組成的,而數組的大小則由常量池計數器指定。
常量池計數器constant_pool_count 的值 =constant_pool表中的成員數+ 1。
constant_pool表的索引值只有在大於 0 且小於constant_pool_count時才會被認為是有效的。
常量池數據區
常量池,constant_pool是一種表結構,它包含 Class 文件結構及其子結構中引用的所有字符串常量、 類或接口名、字段名和其它常量。
常量池中的每一項都具備相同的格式特征——第一個字節作為類型標記用於識別該項是哪種類型的常量,稱為 “tag byte” 。
常量池的索引范圍是1至constant_pool_count−1。常量池的具體細節我們會稍后討論。
NO1.常量池在class文件的什么位置?
NO2.常量池的里面是怎么組織的?
常量池的組織很簡單,前端的兩個字節占有的位置叫做常量池計數器(constant_pool_count),它記錄着常量池的組成元素常量池項(cp_info)的個數。緊接着會排列着constant_pool_count-1個常量池項(cp_info)。如下圖所示:
NO3.常量池項 (cp_info) 的結構是什么?
每個常量池項(cp_info) 都會對應記錄着class文件中的某中類型的字面量。讓我們先來了解一下常量池項(cp_info)的結構:
JVM虛擬機規定了不同的tag值和不同類型的字面量對應關系如下:
所以根據cp_info中的tag 不同的值,可以將cp_info 更細化為以下結構體:
CONSTANT_Utf8_info,CONSTANT_Integer_info,
CONSTANT_Float_info,CONSTANT_Long_info,
CONSTANT_Double_info,CONSTANT_Class_info,
CONSTANT_String_info,CONSTANT_Fieldref_info,
CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info,
CONSTANT_NameAndType_info,CONSTANT_MethodHandle_info,
CONSTANT_MethodType_info,CONSTANT_InvokeDynamic_info。
現在讓我們看一下細化了的常量池的結構會是類似下圖所示的樣子:
NO4.常量池能夠表示那些信息?
NO5. int和float數據類型的常量在常量池中是怎樣表示和存儲的?(CONSTANT_Integer_info, CONSTANT_Float_info)
Java語言規范規定了 int類型和Float 類型的數據類型占用 4 個字節的空間。相應地,在常量池中,將 int和Float類型的常量分別使用CONSTANT_Integer_info和 Constant_float_info表示,他們的結構如下所示:
舉例:建下面的類 IntAndFloatTest.java,在這個類中,我們聲明了五個變量,但是取值就兩種int類型的10 和Float類型的11f。
1 package com.louis.jvm; 2 3 public class IntAndFloatTest { 4 5 private final int a = 10; 6 private final int b = 10; 7 private float c = 11f; 8 private float d = 11f; 9 private float e = 11f; 10 11 }
然后用編譯器編譯成IntAndFloatTest.class字節碼文件,我們通過javap -v IntAndFloatTest 指令來看一下其常量池中的信息,可以看到雖然我們在代碼中寫了兩次10 和三次11f,但是常量池中,就只有一個常量10 和一個常量11f,如下圖所示:
從結果上可以看到常量池第#8 個常量池項(cp_info) 就是CONSTANT_Integer_info,值為10;第#23個常量池項(cp_info) 就是CONSTANT_Float_info,值為11f。
代碼中所有用到 int 類型 10 的地方,會使用指向常量池的指針值#8 定位到第#8 個常量池項(cp_info),即值為 10的結構體CONSTANT_Integer_info。
而用到float類型的11f時,也會指向常量池的指針值#23來定位到第#23個常量池項(cp_info) 即值為11f的結構體CONSTANT_Float_info。如下圖所示:
NO6. long和 double數據類型的常量在常量池中是怎樣表示和存儲的?(CONSTANT_Long_info、CONSTANT_Double_info )
Java語言規范規定了 long 類型和 double類型的數據類型占用8 個字節的空間。相應地,在常量池中,將long和double類型的常量分別使用CONSTANT_Long_info和Constant_Double_info表示,他們的結構如下所示:
舉例:建下面的類 LongAndDoubleTest.java,在這個類中,我們聲明了六個變量,但是取值就兩種Long 類型的-6076574518398440533L 和Double 類型的10.1234567890D。
package com.louis.jvm; public class LongAndDoubleTest { private long a = -6076574518398440533L; private long b = -6076574518398440533L; private long c = -6076574518398440533L; private double d = 10.1234567890D; private double e = 10.1234567890D; private double f = 10.1234567890D; }
然后用編譯器編譯成 LongAndDoubleTest.class 字節碼文件,我們通過javap -v LongAndDoubleTest指令來看一下其常量池中的信息。
可以看到雖然我們在代碼中寫了三次-6076574518398440533L 和三次10.1234567890D,但是常量池中,就只有一個常量-6076574518398440533L 和一個常量10.1234567890D,如下圖所示:
從結果上可以看到常量池第 #18 個常量池項(cp_info) 就是CONSTANT_Long_info,值為-6076574518398440533L ;第 #26個常量池項(cp_info) 就是CONSTANT_Double_info,值為10.1234567890D。
代碼中所有用到 long 類型-6076574518398440533L 的地方,會使用指向常量池的指針值#18 定位到第 #18 個常量池項(cp_info),即值為-6076574518398440533L 的結構體CONSTANT_Long_info。
而用到double類型的10.1234567890D時,也會指向常量池的指針值#26來定位到第 #26 個常量池項(cp_info) 即值為10.1234567890D的結構體CONSTANT_Double_info。如下圖所示:
NO7. String類型的字符串常量在常量池中是怎樣表示和存儲的?(CONSTANT_String_info、CONSTANT_Utf8_info)
對於字符串而言,JVM會將字符串類型的字面量以UTF-8 編碼格式存儲到在class字節碼文件中。
我們先從直觀的Java源碼中中出現的用雙引號"" 括起來的字符串來看,在編譯器編譯的時候,都會將這些字符串轉換成CONSTANT_String_info結構體,然后放置於常量池中。其結構如下所示:
如上圖所示的結構體,CONSTANT_String_info結構體中的string_index的值指向了CONSTANT_Utf8_info結構體,而字符串的utf-8編碼數據就在這個結構體之中。如下圖所示:
請看一例,定義一個簡單的StringTest.java類,然后在這個類里加一個"JVM原理" 字符串,然后,我們來看看它在class文件中是怎樣組織的。
package com.louis.jvm; public class StringTest { private String s1 = "JVM原理"; private String s2 = "JVM原理"; private String s3 = "JVM原理"; private String s4 = "JVM原理"; }
將Java源碼編譯成StringTest.class文件后,在此文件的目錄下執行 javap -v StringTest 命令,會看到如下的常量池信息的輪廓:
(PS :使用javap -v 指令能看到易於我們閱讀的信息,查看真正的字節碼文件可以使用HEXWin、NOTEPAD++、UtraEdit 等工具。)
在面的圖中,我們可以看到CONSTANT_String_info結構體位於常量池的第#15個索引位置。而存放"Java虛擬機原理" 字符串的 UTF-8編碼格式的字節數組被放到CONSTANT_Utf8_info結構體中,該結構體位於常量池的第#16個索引位置。
上面的圖只是看了個輪廓,讓我們再深入地看一下它們的組織吧。請看下圖:
由上圖可見:“JVM原理”的UTF-8編碼的數組是:4A564D E5 8E 9FE7 90 86,並且存入了CONSTANT_Utf8_info結構體中。
NO8. 類文件中定義的類名和類中使用到的類在常量池中是怎樣被組織和存儲的?(CONSTANT_Class_info)
JVM會將某個Java 類中所有使用到了的類的完全限定名 以二進制形式的完全限定名封裝成CONSTANT_Class_info結構體中,然后將其放置到常量池里。CONSTANT_Class_info 的tag值為 7 。其結構如下:
(PS:在某個Java源碼中,我們會使用很多個類,比如我們定義了一個 ClassTest的類,並把它放到com.louis.jvm 包下,則 ClassTest類的完全限定名為com.louis.jvm.ClassTest,將JVM編譯器將類編譯成class文件后,此完全限定名在class文件中,是以二進制形式的完全限定名存儲的,即它會把完全限定符的"."換成"/" ,即在class文件中存儲的 ClassTest類的完全限定名稱是"com/louis/jvm/ClassTest"。因為這種形式的完全限定名是放在了class二進制形式的字節碼文件中,所以就稱之為 二進制形式的完全限定名。)
舉例,我們定義一個很簡單的ClassTest類,來看一下常量池是怎么對類的完全限定名進行存儲的。
package com.jvm; import java.util.Date; public class ClassTest { private Date date =new Date(); }
將Java源碼編譯成ClassTest.class文件后,在此文件的目錄下執行 javap -v ClassTest 命令,會看到如下的常量池信息的輪廓:
如上圖所示,在ClassTest.class文件的常量池中,共有 3 個CONSTANT_Class_info結構體,分別表示ClassTest 中用到的Class信息。
我們就看其中一個表示com/jvm.ClassTest的CONSTANT_Class_info 結構體。它在常量池中的位置是#1,它的name_index值為#2,它指向了常量池的第2 個常量池項,如下所示:
對於某個類而言,其class文件中至少要有兩個CONSTANT_Class_info常量池項,用來表示自己的類信息和其父類信息。
(除了java.lang.Object類除外,其他的任何類都會默認繼承自java.lang.Object)
如果類聲明實現了某些接口,那么接口的信息也會生成對應的CONSTANT_Class_info常量池項。
除此之外,如果在類中使用到了其他的類,只有真正使用到了相應的類,JDK編譯器才會將類的信息組成CONSTANT_Class_info常量池項放置到常量池中。如下圖:
package com.louis.jvm; import java.util.Date; public class Other{ private Date date; public Other() { Date da; } }
上述的Other的類,在JDK將其編譯成class文件時,常量池中並沒有java.util.Date對應的CONSTANT_Class_info常量池項,為什么呢?
在Other類中雖然定義了Date類型的兩個變量date、da,但是JDK編譯的時候,認為你只是聲明了“Ljava/util/Date”類型的變量,並沒有實際使用到Ljava/util/Date類。
將類信息放置到常量池中的目的,是為了在后續的代碼中有可能會反復用到它。很顯然,JDK在編譯Other類的時候,會解析到Date類有沒有用到,發現該類在代碼中就沒有用到過,所以就認為沒有必要將它的信息放置到常量池中了。
將上述的Other類改寫一下,僅使用new Date(),如下圖所示:
package com.louis.jvm; import java.util.Date; public class Other{ public Other() { new Date(); } }
這時候使用javap -v Other ,可以查看到常量池中有表示java/util/Date的常量池項:
NO9.類中引用到的field字段在常量池中是怎樣描述的?(CONSTANT_Fieldref_info, CONSTANT_Name_Type_info)
一般而言,我們在定義類的過程中會定義一些 field 字段,然后會在這個類的其他地方(如方法中)使用到它。有可能我們在類的方法中只使用field字段一次,也有可能我們會在類定義的方法中使用它很多很多次。
舉一個簡單的例子,我們定一個叫Person的簡單java bean,它有name和age兩個field字段,如下所示:
package com.louis.jvm; public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
在上面定義的類中,我們在Person類中的一系列方法里,多次引用到namefield字段和agefield字段。
對於JVM編譯器而言,name和age只是一個符號而已,並且它在由於它可能會在此類中重復出現多次,所以JVM把它當作常量來看待,將name和age以field字段常量的形式保存到常量池中。
將它name和age封裝成CONSTANT_Fieldref_info常量池項,放到常量池中,在類中引用到它的地方,直接放置一個指向field字段所在常量池的索引。
上面的Person類,使用javap -v Person指令,查看class文件的信息,你會看到,在Person類中引用到age和namefield字段的地方,都是指向了常量池中age和namefield字段對應的常量池項中。表示field字段的常量池項叫做CONSTANT_Fieldref_info。
怎樣描述某一個field字段的引用?
實例解析: 現在,讓我們來看一下Person類中定義的namefield字段在常量池中的表示。通過使用javap -v Person會查看到如下的常量池信息:
請讀者看上圖中namefield字段的數據類型,它在#6個常量池項,以UTF-8編碼格式的字符串“Ljava/lang/String;” 表示,這表示着這個field 字段是Java.lang.String 類型的。
關於field字段的數據類型,class文件中存儲的方式和我們在源碼中聲明的有些不一樣。請看下圖的對應關系:
(PS:如果我們在類中定義了field 字段,但是沒有在類中的其他地方用到這些字段,它是不會被編譯器放到常量池中的。讀者可以自己試一下。(當然了,定義了但是沒有在類中的其它地方引用到這種情況很少。))
(PS:只有在類中的其他地方引用到了,才會將他放到常量池中。)
NO10.類中引用到的method方法在常量池中是怎樣描述的?(CONSTANT_Methodref_info, CONSTANT_Name_Type_info)
1.舉例:
還是以Person類為例。在Person類中,我們定義了setName(String name)、getName()、setAge(int age)、getAge()這些方法:
package com.louis.jvm; public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
雖然我們定義了方法,但是這些方法沒有在類總的其他地方被用到(即沒有在類中其他的方法中引用到),所以它們的方法引用信息並不會放到常量中。
現在我們在類中加一個方法 getInfo(),調用了getName()和getAge() 方法:
public String getInfo() { return getName()+"\t"+getAge(); }
這時候JVM編譯器會將getName()和getAge()方法的引用信息包裝成CONSTANT_Methodref_info結構體放入到常量池之中。
(PS: 這里的方法調用的方式牽涉到Java非常重要的一個術語和機制,叫動態綁定。這個動態綁定問題以后在單獨談談。)
2. 怎樣表示一個方法引用?
3. 方法描述符的組成
4. getName() 方法引用在常量池中的表示
NO11.類中引用到某個接口中定義的method方法在常量池中是怎樣描述的?(CONSTANT_InterfaceMethodref_info,CONSTANT_Name_Type_info)
當我們在某個類中使用到了某個接口中的方法,JVM會將用到的接口中的方法信息放到這個類的常量池中。
比如我們定義了一個Worker接口,和一個Boss類,在Boss類中調用了Worker接口中的方法,這時候在Boss類的常量池中會有Worker接口的方法的引用表示。
1 package com.louis.jvm; 2 3 /** 4 * Worker 接口類 5 * @author luan louis 6 */ 7 public interface Worker{ 8 9 public void work(); 10 11 }
1 package com.louis.jvm; 2 3 /** 4 * Boss 類,makeMoney()方法 調用Worker 接口的work 5 * @author louluan 6 */ 7 public class Boss { 8 9 public void makeMoney(Worker worker) 10 { 11 worker.work(); 12 } 13 14 }
我們對Boss.class執行javap -v Boss,然后會看到如下信息:
如上圖所示,在Boss類的makeMoney()方法中調用了Worker接口的work()方法,機器指令是通過invokeinterface指令完成的。
invokeinterface指令后面的操作數,是指向了Boss常量池中Worker接口的work()方法描述,表示的意思就是:“我要調用Worker接口的work()方法”。
Worker接口的work()方法引用信息,JVM會使用CONSTANT_InterfaceMethodref_info結構體來描述,CONSTANT_InterfaceMethodref_info定義如下:
CONSTANT_InterfaceMethodref_info結構體和上面介紹的CONSTANT_Methodref_info 結構體很基本上相同,它們的不同點只有:
1.CONSTANT_InterfaceMethodref_info 的tag 值為11,而CONSTANT_Methodref_info的tag值為10;
2. CONSTANT_InterfaceMethodref_info 描述的是接口中定義的方法,而CONSTANT_Methodref_info描述的是實例類中的方法;
NO12.CONSTANT_MethodType_info,CONSTANT_MethodHandle_info,CONSTANT_InvokeDynamic_info
這三項主要是為了讓Java語言支持動態語言特性而在Java 7 版本中新增的三個常量池項,只會在極其特別的情況能用到它,在class文件中幾乎不會生成這三個常量池項。
總結:
1.對於某個類或接口而言,其自身、父類和繼承或實現的接口的信息會被直接組裝成CONSTANT_Class_info常量池項放置到常量池中;
2. 類中或接口中使用到了其他的類,只有在類中實際使用到了該類時,該類的信息才會在常量池中有對應的CONSTANT_Class_info常量池項;
3. 類中或接口中僅僅定義某種類型的變量,JDK只會將變量的類型描述信息以UTF-8字符串組成CONSTANT_Utf8_info常量池項放置到常量池中,上面在類中的private Date date;JDK編譯器只會將表示date的數據類型的“Ljava/util/Date”字符串放置到常量池中。
本文源自 http://blog.csdn.NET/luanlouis/