解析常量池
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] 字符串实际上是通过字节数组存储的