bboyjing's blog

自己动手写JVM六【解析class文件(四)】

本章节继续学习ClassFile中各项所代表的含义。

常量池

常量池占据了class文件很大一部分数据,里面存储着各式各样的常量信息,包括数字和字符串常量、类和接口名,字段和方法等等,下面我们就来详细了解常量池和各种常量。

ConstantPool结构体

constant_pool.go文件中定义了ConstantPool类型,常量池实际上也是一个表,所以ConstantPool被定义成ConstantInfo的数组。所以,要理解常量池,得先去看ConstantInfo是什么,然后再回过来,此处未完待续,接着往下看。

ConstantInfo

由于常量池中存放的信息各不相同,所以每种常量的格式不同。常量数据的第一个字节时tag,用来区分常量的类型,下面是Java虚拟机规范给出的常量结构:

1
2
3
4
cp_info {
u1 tag;
u1 info[];
}

Java虚拟机一共定义了14中常量,虽然篇幅比较长,但为了看的更清楚,还是列举下吧:

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

ConstantInfo被定义成一个接口,位于constant_info.go文件中,至于为什么ConstantInfo是个接口,看看上面这14种类型就明白了。下面在constant_info.go文件中定义对应tag的常量值,顺便把ConstantInfo接口也贴出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const (
CONSTANT_Utf8 = 1
CONSTANT_Integer = 3
CONSTANT_Float = 4
CONSTANT_Long = 5
CONSTANT_Double = 6
CONSTANT_Class = 7
CONSTANT_String = 8
CONSTANT_Fieldref = 9
CONSTANT_Methodref = 10
CONSTANT_InterfaceMethodref = 11
CONSTANT_NameAndType = 12
CONSTANT_MethodHandle = 15
CONSTANT_MethodType = 16
CONSTANT_InvokeDynamic = 18
)
type ConstantInfo interface {
// 读取常量信息,由具体的常量结构体实现
readInfo(reader *ClassReader)
}

继续编辑constant_info.go,添加读取常量信息入口:

1
2
3
func readConstantInfo(reader *ClassReader, cp ConstantPool) {
}

由于读取常量信息由具体的常量结构体实现,目前还没有任何实现,所以没办法继续下去了,下面要做的就是去理解并实现14种常量结构。

CONSTANT_Integer_info

CONSTANT_Integer_info使用4个字节存储整数常量,tag占一个字节,一共5个字节。其结构定义如下:

1
2
3
4
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}

CONSTANT_Integer_info和后面将要介绍的三种数字常量无论结构还是实现都非常相似,所以把它们都定义在cp_numeric.go文件中,ConstantIntegerInfo相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 读取CONSTANT_Integer_info
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {
// 读取4个字节
bytes := reader.readUint32()
// 将bytes转为int32
self.val = int32(bytes)
}
// 获取int32常量值
func (self *ConstantIntegerInfo) Value() int32 {
return self.val
}

CONSTANT_Integer_info正好可以容纳一个Java的int型常量,但实际上比int更小的boolean、byte、short和char类型的常量也放在CONSTANT_Integer_info。ClassFile.class的int常量class文件如下:
jvmgo_13
由上图可以看出,tag为0x03,表示CONSTANT_Integer_info。bytes为0x075BCD15,转换成十进制正好是123456789。

CONSTANT_Float_info

CONSTANT_Float_info使用4字节存储IEEE754单精度浮点数常量,tag占一个字节,一共5个字节。其结构和ConstantIntegerInfo非常类似,这里就不给出代码了,在cp_numeric.go文件中可以查看。

CONSTANT_Long_info

CONSTANT_Long_info使用8个字节存储整数常量,tag占一个字节,一共9个字节。其结构和ConstantIntegerInfo非常类似,这里就不给出代码了,在cp_numeric.go文件中可以查看。

CONSTANT_Double_info

CONSTANT_Double_info使用8个字节存储IEEE754双精度浮点数,tag占一个字节,一共9个字节。其结构和ConstantIntegerInfo非常类似,这里就不给出代码了,在cp_numeric.go文件中可以查看。

CONSTANT_Utf8_info

CONSTANT_Utf8_info常量里放的是MUTF-8编码的字符串,结构如下:

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

要注意的是,字符串在class文件中是以MUTF-8(Modified UTF-8)方式编码的,与正常的UTF-8是有点区别的,具体这里就不讨论了。ConstantUtf8Info位于cp_utf8.go中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type ConstantUtf8Info struct {
str string
}
// 读取CONSTANT_Utf8_info
func (self *ConstantUtf8Info) readInfo(reader *ClassReader) {
// 读取length(2个字节),并转换成uint32
length := uint32(reader.readUint16())
// 读取指定长度的字节
bytes := reader.readBytes(length)
// 将字节转换成MUTF-8
self.str = decodeMUTF8(bytes)
}
// 获取string常量值
func (self *ConstantUtf8Info) Str() string {
return self.str
}

上面代码中decodeMUTF8方法篇幅比较长,是书的作者参照java.io.DataInputStream.readUTF(DataInput)方法写的,这里就不贴出来了,文件中有。像字段名、字段描述符等就是以字符串的形式存储在class文件中的,比如字段PI对应的class文件如下图所示:
jvmgo_14
上图可以看出,tag为0x01,表示CONSTANT_Utf8_info。length为0x0002,表示bytes的长度为2,所以bytes的值为接下来的后2个字节0x5049,对应的ASCII码正是PI。

下节继续。。。