- Java虛擬機字節碼:從入門到實戰
- 吳就業
- 3813字
- 2021-01-08 19:08:25
解析常量池
Java虛擬機執行字節碼指令依賴常量池表中的常量信息,如創建對象的new指令,需要指定對象的類型描述符,new指令的操作數的值為該類型描述符對應的常量在常量池中的索引,虛擬機將根據常量信息到方法區尋找對應的class元數據。
根據《Java虛擬機規范》規定,常量池表中所有常量項的結構都包含一個tag項,tag值用于標志一個常量是哪種常量結構。只有根據tag確定常量的結構,才能根據常量結構計算常量所占用的字節數。常量結構的通用格式為:
cp_info{ u1 tag; u1 info[]; }
其中U1類型占一個字節。Info[]字節數組存儲的內容和長度由tag值決定。tag值對應的常量結構如表2-11所示。
表2-11 常量池tag與常量結構映射表

要從class文件中解析出常量池中的所有項,除了要了解每個tag值對應的常量結構之外,我們還需要了解每個常量結構都用于存儲哪些信息,才能確定每個常量所占用的字節數。在實現常量池解析器之前,我們需要先根據《Java虛擬機規范》中描述的每個常量結構創建對應的Java類型。
常量池各項的解析
與class文件結構的各項解析器一樣,我們也要求每個常量結構都要實現各自的解析工作。首先定義常量解析器接口ConstantInfoHandler,如代碼清單2-12所示。
代碼清單2-12 ConstantInfoHandler常量類型解析器接口
public interface ConstantInfoHandler { /** * 讀取 * @param codeBuf */ void read(ByteBuffer codeBuf) throws Exception; }
ConstantInfoHandler接口只定義一個解析方法,方法要求傳入class文件字節緩存。該class文件字節緩存與class文件結構各項解析器使用的是同一個緩存對象,都是從同一個class文件讀取到內存中的ByteBuffer對象。
接著根據常量結構的通用格式將常量結構抽象出一個父類CpInfo,CpInfo類的實現如代碼清單2-13所示。
代碼清單2-13 CpInfo類
public abstract class CpInfo implements ConstantInfoHandler { private U1 tag; public CpInfo(U1 tag) { this.tag = tag; } @Override public String toString() { return "tag=" + tag.toHexString(); } }
CpInfo抽象類約定構建方法必須傳入tag值,且約定子類必須實現ConstantInfoHandler常量結構解析器接口,并實現常量解析方法。
CONSTANT_Utf8_info
根據《Java虛擬機規范》規定,CONSTANT_Utf8_info常量結構用于存儲字符串常量,字符串編碼使用UTF-8。除一個必須的tag字段和存儲字符串的字節數組外,還要有一個字段存儲描述這個字符串字節數組的長度[2]。
創建CONSTANT_Utf8_Info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法。如代碼清單2-14所示。
代碼清單2-14 CONSTANT_Utf8_info類
public class CONSTANT_Utf8_info extends CpInfo { private U2 length; private byte[] bytes; public CONSTANT_Utf8_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { // 先讀取長度 length = new U2(codeBuf.get(), codeBuf.get()); bytes = new byte[length.toInt()]; // 讀取指定長度的字節到bytes數組 codeBuf.get(bytes, 0, length.toInt()); } @Override public String toString() { return super.toString() + ",length=" + length.toInt() + ",str=" + new String(bytes, StandardCharsets.UTF_8); } }
代碼清單2-14所示,read方法在從Class文件字節緩存ByteBuffer對象中讀取該類型的常量時,需按順序先讀取長度,再根據長度n取后續n個字節存放到該常量的字節數組中。
CONSTANT_Class_Info
根據《Java虛擬機規范》規定,CONSTANT_Class_Info常量存儲類的符號信息,除tag字段外,只有一個存儲指向常量池表中某一常量的索引字段name_index,name_index指向的常量必須是一個CONSTANT_Utf8_info常量,該常量存儲class的類名(內部類名[1])。
創建CONSTANT_Class_Info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-15所示。
代碼清單2-15 CONSTANT_Utf8_info
public class CONSTANT_Class_info extends CpInfo { private U2 name_index; public CONSTANT_Class_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { // 讀取兩個字節 this.name_index = new U2(codeBuf.get(), codeBuf.get()); } }
CONSTANT_Fieldref_info
根據《Java虛擬機規范》規定,CONSTANT_Fieldref_info常量存儲字段的符號信息,除tag字段外,有兩個U2類型的指向常量池中某個常量的索引字段,分別是class_index、name_and_type_index。
CONSTANT_Fieldref_info結構的各項說明:
◆class_index指向的常量必須是一個CONSTANT_Class_Info常量,表示當前字段所在的類的類名;
◆name_and_type_index指向的常量必須是一個CONSTANT_NameAndType_info常量,表示當前字段的名字和類型描述符。
創建CONSTANT_Fieldref_info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-16所示。
代碼清單2-16 CONSTANT_Fieldref_info類
public class CONSTANT_Fieldref_info extends CpInfo { private U2 class_index; private U2 name_and_type_index; public CONSTANT_Fieldref_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { class_index = new U2(codeBuf.get(), codeBuf.get()); name_and_type_index = new U2(codeBuf.get(), codeBuf.get()); } }
CONSTANT_Methodref_info
CONSTANT_Methodref_info在結構上與CONSTANT_Fieldref_info一樣,因此可通過繼承CONSTANT_Fieldref_info類實現其字段的定義和完成解析工作。
CONSTANT_Methodref_info結構的各項說明:
◆class_index指向的常量必須是一個CONSTANT_Class_Info常量,表示當前方法所在的類的類名;
◆name_and_type_index指向的常量必須是一個CONSTANT_NameAndType_info常量,表示當前方法的名字和方法描述符。
創建CONSTANT_Methodref_info類并繼承CONSTANT_Fieldref_info,如代碼清單2-17所示。
代碼清單2-17 CONSTANT_Methodref_info類
public class CONSTANT_Methodref_info extends CONSTANT_Fieldref_info { public CONSTANT_Methodref_info(U1 tag) { super(tag); } }
CONSTANT_InterfaceMethodref_info
CONSTANT_InterfaceMethodref_info在結構上與CONSTANT_Fieldref_info一樣,因此可通過繼承CONSTANT_Fieldref_info類實現其字段的定義和完成解析工作。
CONSTANT_InterfaceMethodref_info結構的各項說明:
◆class_index指向的常量必須是一個CONSTANT_Class_Info常量,表示當前接口方法所屬的接口的類名;
◆name_and_type_index指向的常量必須是一個CONSTANT_NameAndType_info常量,表示當前接口方法的名字和方法描述符。
創建CONSTANT_InterfaceMethodref_info類并繼承CONSTANT_Fieldref_info,如代碼清單2-18所示。
代碼清單2-18 CONSTANT_InterfaceMethodref_info類
public class CONSTANT_InterfaceMethodref_info extends CONSTANT_Fieldref_info{ public CONSTANT_InterfaceMethodref_info(U1 tag) { super(tag); } }
CONSTANT_String_info
根據《Java虛擬機規范》規定,CONSTANT_String_info結構存儲Java中String類型的常量,除tag字段外,還有一個U2類型的字段string_index,值為常量池中某個常量的索引,該索引指向的常量必須是一個CONSTANT_Utf8_info常量。
創建CONSTANT_String_info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-19所示。
代碼清單2-19 CONSTANT_String_info類
public class CONSTANT_String_info extends CpInfo { private U2 string_index; public CONSTANT_String_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { string_index = new U2(codeBuf.get(), codeBuf.get()); } }
CONSTANT_Integer_info
根據《Java虛擬機規范》規定,CONSTANT_Integer_info常量存儲一個整型數值,除一個tag字段外,只有一個U4類型的字段bytes,bytes轉為10進制數就是這個常量所表示的整型值。
創建CONSTANT_Integer_info類并繼承Cpinfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-20所示。
代碼清單2-20 CONSTANT_Integer_info類
public class CONSTANT_Integer_info extends CpInfo { private U4 bytes; public CONSTANT_Integer_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { // 連續讀取四個字節 bytes = new U4(codeBuf.get(),codeBuf.get(),codeBuf.get(),codeBuf.get()); } }
CONSTANT_Float_info
CONSTANT_Float_info與CONSTANT_Integer_info在存儲結構上是一樣的,只是bytes所表示的內容不同,CONSTANT_Float_info的bytes存儲的是浮點數。
CONSTANT_Float_info類的定義和解析方法的實現也可通過繼承CONSTANT_Integer_info實現。如代碼清單2-21所示。
代碼清單2-21 CONSTANT_Float_info類
public class CONSTANT_Float_info extends CONSTANT_Integer_info { public CONSTANT_Float_info(U1 tag) { super(tag); } }
CONSTANT_Long_info
與CONSTANT_Integer_info常量不同的是,CONSTANT_Long_info常量使用8個字節存儲一個長整型數值,即使用兩個U4類型的字段分別存儲一個長整型數的高32位和低32位。
創建CONSTANT_Long_info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-22所示。
代碼清單2-22 CONSTANT_Long_info類
public class CONSTANT_Long_info extends CpInfo { private U4 hight_bytes; private U4 low_bytes; public CONSTANT_Long_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { // 讀取高32位 hight_bytes = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get()); // 讀取低32位 low_bytes = new U4(codeBuf.get(), codeBuf.get(), codeBuf.get(), codeBuf.get()); } }
CONSTANT_Double_info
CONSTANT_Double_info常量與CONSTANT_Long_info常量在結構上也是一樣的,只是所表示的值類型不同。因此CONSTANT_Double_info類的定義和解析方法的實現也可通過繼承CONSTANT_Long_info實現。如代碼清單2-23所示。
代碼清單2-23 CONSTANT_Double_info類
public class CONSTANT_Double_info extends CONSTANT_Long_info { public CONSTANT_Double_info(U1 tag) { super(tag); } }
CONSTANT_NameAndType_info
根據《Java虛擬機規范》規定,CONSTANT_NameAndType_info結構用于存儲字段的名稱和字段的類型描述符,或者是用于存儲方法的名稱和方法的描述符。關于描述符和簽名放在第三章介紹。
CONSTANT_NameAndType_info結構除tag字段外,還有一個U2類型的字段name_index和一個U2類型的字段descriptor_index,分別對應名稱指向常量池中某個常量的索引和描述符指向常量池中某個常量的索引,這兩個字段指向的常量都必須是CONSTANT_Utf8_info結構的常量。
創建CONSTANT_NameAndType_info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-24所示。
代碼清單2-24 CONSTANT_NameAndType_info類
public class CONSTANT_NameAndType_info extends CpInfo { private U2 name_index; private U2 descriptor_index; public CONSTANT_NameAndType_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { // 名稱索引 name_index = new U2(codeBuf.get(), codeBuf.get()); // 描述符索引 descriptor_index = new U2(codeBuf.get(), codeBuf.get()); } }
CONSTANT_MethodHandle_info
根據《Java虛擬機規范》規定,CONSTANT_MethodHandle_info結構用于存儲方法句柄,這是虛擬機為實現動態調用invokedynamic指令所增加的常量結構。CONSTANT_MethodHandle_info結構除必須的tag字段外,有一個U1類型的字段reference_kind,取值范圍為1~9,包括1和9,表示方法句柄的類型,還有一個U1類型的字段reference_index,其值為指向常量池中某個常量的索引。
reference_index指向的常量的結構與reference_kind取值的關系如表2-25所示。
表2-25 reference_index與reference_kind的關系映射表

創建CONSTANT_MethodHandle_info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-26所示。
代碼清單2-26 CONSTANT_MethodHandle_info類
public class CONSTANT_MethodHandle_info extends CpInfo { private U1 reference_kind; private U2 reference_index; public CONSTANT_MethodHandle_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { reference_kind = new U1(codeBuf.get()); reference_index = new U2(codeBuf.get(), codeBuf.get()); } }
CONSTANT_MethodType_info
CONSTANT_MethodType_info結構表示方法類型,與CONSTANT_MethodHandle_info結構一樣,也是虛擬機為實現動態調用invokedynamic指令所增加的常量結構。
CONSTANT_MethodType_info除tag字段外,只有一個u2類型的描述符指針字段descriptor_index,指向常量池中的某一CONSTANT_Utf8_info結構的常量。
創建CONSTANT_MethodType_info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-27所示。
代碼清單2-27 CONSTANT_MethodType_info類
public class CONSTANT_MethodType_info extends CpInfo { private U2 descriptor_index; public CONSTANT_MethodType_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { descriptor_index = new U2(codeBuf.get(), codeBuf.get()); } }
CONSTANT_InvokeDynamic_info
CONSTANT_InvokeDynamic_info表示invokedynamic指令用到的引導方法bootstrap method以及引導方法所用到的動態調用名稱、參數、返回類型。
CONSTANT_InvokeDynamic_info結構除tag字段外,有兩個U2類型的字段,分別是bootstrap_method_attr_index和name_and_type_index,前者指向class文件結構屬性表中引導方法表的某個引導方法,后者指向常量池中某個CONSTANT_NameAndType_Info結構的常量。
創建CONSTANT_InvokeDynamic_info類并繼承CpInfo抽象類,實現ConstantInfoHandler接口定義的解析方法,如代碼清單2-28所示。
代碼清單2-28 CONSTANT_InvokeDynamic_info類
public class CONSTANT_InvokeDynamic_info extends CpInfo { private U2 bootstrap_method_attr_index; private U2 name_and_type_index; public CONSTANT_InvokeDynamic_info(U1 tag) { super(tag); } @Override public void read(ByteBuffer codeBuf) throws Exception { bootstrap_method_attr_index = new U2(codeBuf.get(), codeBuf.get()); name_and_type_index = new U2(codeBuf.get(), codeBuf.get()); } }
常量池的解析
在創建完各常量結構對應的Java類,和實現各常量結構的解析方法后,我們再來完成整個常量池的解析工作。
我們先修改所有常量類型(我們編寫的常量類)的父類CpInfo,在CpInfo類中添加一個靜態方法,用于根據tag的值創建不同的常量類型對象。如代碼清單2-29所示。
代碼清單2-29 CpInfo類添加靜態構建方法
public abstract class CpInfo implements ConstantInfoHandler { private U1 tag; protected CpInfo(U1 tag) { this.tag = tag; } @Override public String toString() { return "tag=" + tag.toString(); } public static CpInfo newCpInfo(U1 tag) throws Exception { int tagValue = tag.toInt(); CpInfo info; switch (tagValue) { case 1: info = new CONSTANT_Utf8_info(tag); break; case 3: info = new CONSTANT_Integer_info(tag); break; case 4: info = new CONSTANT_Float_info(tag); break; case 5: info = new CONSTANT_Long_info(tag); break; case 6: info = new CONSTANT_Double_info(tag); break; case 7: info = new CONSTANT_Class_info(tag); break; case 8: info = new CONSTANT_String_info(tag); break; case 9: info = new CONSTANT_Fieldref_info(tag); break; case 10: info = new CONSTANT_Methodref_info(tag); break; case 11: info = new CONSTANT_InterfaceMethodref_info(tag); break; case 12: info = new CONSTANT_NameAndType_info(tag); break; case 15: info = new CONSTANT_MethodHandle_info(tag); break; case 16: info = new CONSTANT_MethodType_info(tag); break; case 18: info = new CONSTANT_InvokeDynamic_info(tag); break; default: throw new Exception("沒有找到該TAG=" + tagValue + "對應的常量類型"); } return info; } }
接著創建常量池解析器ConstantPoolHandler,設置其排序值為版本號解析器的排序值+1,也就是將該解析器排在版本號解析器的后面。ConstantPoolHandler的實現如代碼清單2-30所示。
代碼清單2-30 常量池解析器ConstantPoolHandler
public class ConstantPoolHandler implements BaseByteCodeHandler { @Override public int order() { return 2; } @Override public void read(ByteBuffer codeBuf, ClassFile classFile) throws Exception { // 獲取常量池技術器的值 U2 cpLen = new U2(codeBuf.get(), codeBuf.get()); classFile.setConstant_pool_count(cpLen); // 常量池中常量的總數 int cpInfoLeng = cpLen.toInt() - 1; classFile.setConstant_pool(new CpInfo[cpInfoLeng]); // 解析所有常量 for (int i = 0; i < cpInfoLeng; i++) { U1 tag = new U1(codeBuf.get()); CpInfo cpInfo = CpInfo.newCpInfo(tag); cpInfo.read(codeBuf); System.out.println("#" + (i + 1) + ":" + cpInfo); classFile.getConstant_pool()[i] = cpInfo; } } }
如代碼清單2-30所示,常量池解析器實現BaseByteCodeHandler接口的read方法,在read方法中,首先是讀取常量池計數器,根據常量池計數器減1得到常量池的常量總數,再根據常量總數創建常量池表。最后是按順序解析常量池的各項常量。
在解析常量池的常量時,先從Class文件字節緩存中取一個字節碼,就是tag,根據tag調用CpInfo的靜態方法newCpInfo創建對應常量類型對象,再調用創建出來的常量類型對象的read方法完成該項常量的解析工作。
最后,我們還要將編寫好的常量池解析器交給ClassFileAnalysiser管理,如代碼清單2-31所示。
代碼清單2-31 在ClassFileAnalysiser中注冊常量池解析器
public class ClassFileAnalysiser { private final static List<BaseByteCodeHandler> handlers = new ArrayList<>(); static { ...... handlers.add(new ConstantPoolHandler()); .... } }
現在我們來編寫單元測試驗證解析結果是否正確。為了觀察解析結果,還需要給各CpInfo的子類添加toString方法,打印出直觀的信息。toString方法的實現就省略了。單元測試代碼如代碼清單2-32所示。
代碼清單2-32 常量池解析器單元測試
public class ConstantPoolHandlerTest { @Test public void testConstantPoolHandler() throws Exception { // 讀取class文件,生成ByteBuffer ByteBuffer codeBuf = ClassFileAnalysisMain.readFile("RecursionAlgorithmMain.class"); // 解析class文件 ClassFile classFile = ClassFileAnalysiser.analysis(codeBuf); // 獲取常量池總數 int cp_info_count = classFile.getConstant_pool_count().toInt(); System.out.println("常量池中常量項總數:" + cp_info_count); // 遍歷常量池中的常量 CpInfo[] cpInfo = classFile.getConstant_pool(); for (CpInfo cp : cpInfo) { System.out.println(cp.toString()); } } }
單元測試部分常量的輸出信息如圖2.4所示

圖2.4 常量池解析器單元測試
注釋:
[1] 內部類名(internal name)指的是使用“/”替換“.”的類名,如java/lang/String
[2] 字符串實際上是通過字節數組存儲的
- Web應用系統開發實踐(C#)
- TypeScript Essentials
- What's New in TensorFlow 2.0
- Building a Game with Unity and Blender
- Getting Started with ResearchKit
- TypeScript實戰指南
- C++ 從入門到項目實踐(超值版)
- Spring Boot進階:原理、實戰與面試題分析
- Creating Stunning Dashboards with QlikView
- 區塊鏈項目開發指南
- Raspberry Pi Blueprints
- JavaScript Unit Testing
- C++面向對象程序設計
- SAP HANA Starter
- C語言開發寶典