代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康-JVM全套教程。
1.常量池概述
常量池占据了class文件很大一部分数据,里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名,等等。
常量池:
- 常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。
- 随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石
- 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。
- 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

由上表可见, Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。
- 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
常量池计数器
constant_pool_count (常量池计数器)
- 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。
- 常量池容量计数值(u2类型) :从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项.
- 为什么呢?通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。
常量池表
constant_pool [](常量池)
- constant_pool是一种表结构,以 1 ~ constant_pool_count -1为索引,表明了后面有多少个常量项。
- 常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
- 它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)。
- 对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。(CONSTANT_Integer_info)

jdk7引入的三个新的引用

字面量和符号引用
在对这些常量解读前,我们需要搞清楚几个概念。
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。如下表:

全限定名
jvmgo/book/ClassFileTest这个就是类的全限定名,而全类名是jvmgo.book.ClassFileTest,仅仅是把包名的”.”替换成”/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。
简单名称
简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的FLAG字段的简单名称分别是FLAG
.
描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、 float、int、long、 short、 boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法java.lang.String toString()的描述符为() Ljava/lang/String;,方法int abc(int[] x, int y)的描述符为([II) I。
如:
package com.yutian.java1;
public class ArrayTest {
public static void main(String[] args) {
Object[] arr = new Object[10];
System.out.println(arr);
String[] arr1 = new String[10];
System.out.println(arr1);
long[][] arr2 = new long[10][];
System.out.println(arr2);
}
}输出结果:

补充说明:虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
这里说明下符号引用和直接引用的区别与关联:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

解读方式:
在常量池计数器后开始,开头两个数字,转换成十进制。就是10.

以此数,再在上图,找到对应标志数,如0a表示10,找到10,就是CONSTANT_Methodref_info,CONSTANT_Methodref_info项,还有两个字节表示指向声明方法的接口描述符CONSTANT Class Info的索引项和两个字节表示指向名称及类型描述符CONSTANT_NameAndType的索引项。再通过索引在常量池中找到对应的值。
用jclasslib一目了然:

细节说明:
- CONSTANT_Class_info 结构用于表示类或接口
- CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info结构表示字段、方法和接口方法
- CONSTANT_String_info结构用于表示String类型的常量对象
- CONSTANT_Integer_info和CONSTANT_Float_info 表示4字节(int和float)的数值常量
- CONSTANT_Long_info和CONSTANT_Double_info结构表示8字节(long和double)的数值常量
- 在class文件的常量池表中,所有的8字节常量均占两个表成员(项)的空间。如果一个CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池表中的索引位n,则常量池表中下一个可用项的索引位n+2,此时常量池表中索引为n+1的项仍然有效但必须视为不可用的。
- CONSTANT_NameAndType_info结构用于表示字段或方法,但是和之前的3个结构不同, CONSTANT_NameAndType_info结构没有指明该字段或方法所属的类或接口。
- CONSTANT_Utf8_info用于表示字符常量的值
- CONSTANT_MethodHandle_info结构用于表示方法句柄
- CONSTANT_MethodType_info结构表示方法类型
- CONSTANT_InvokeDynamic_info结构用于表示invokedynamic指令所用到的引导方法(bootstrap method)、引导方法所用到的动态调用名称(dynamicinvocation name)、参数和返回类型,并可以给引导方法传入一系列称为静态参数(static argument)的常量。
总结:
- 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。
- 在常量池列表中, CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。
- 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8编码,就可以知道其长度。
- 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。
- 常量池中为什么要包含这些内容?Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态链接的内容,在虚拟机类加载过程时再进行详细讲解。
下面我们就使用代码来实现它们
2.ConstantPool结构体
在classfile目录下创建constant_pool.go文件,在里面定义ConstantPool类型,代码如下所示:
整体代码
package classfile
type ConstantPool []ConstantInfo
func readConstantPool(reader *ClassReader) ConstantPool {
cpCount := int(reader.readUint16())
cp := make([]ConstantInfo, cpCount)
for i := 1; i < cpCount; i++ {
cp[i] = readConstantInfo(reader, cp)
switch cp[i].(type) {
case *ConstantLongInfo, *ConstantDoubleInfo:
i++
}
}
return cp
}
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {
if cpInfo := self[index]; cpInfo != nil {
return cpInfo
}
panic("Invalid constant pool index!")
}
func (self ConstantPool) getNameAndType(index uint16) (string, string) {
ntInfo := self.getConstantInfo(index).(*ConstantNameAndTypeInfo)
name := self.getUtf8(ntInfo.nameIndex)
_type := self.getUtf8(ntInfo.descriptorIndex)
return name, _type
}
func (self ConstantPool) getClassName(index uint16) string {
classInfo := self.getConstantInfo(index).(*ConstantClassInfo)
return self.getUtf8(classInfo.nameIndex)
}
func (self ConstantPool) getUtf8(index uint16) string {
utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info)
return utf8Info.str
}
代码解释
注意,如上所述
- 表头给出的常量池大小比实际大1。假设表头给出的值是n,那么常量池的实际大小是n–1。
- 有效的常量池索引是1~n–1。0是无效索引,表示不指向任何常量。
- CONSTANT_Long_info和CONSTANT_Double_info各占两个位置。如果常量池中存在这两种常量,实际的常量数量比n–1还要少,而且1~n–1的某些数也会变成无效索引。
常量池由readConstantPool函数读取:
.(type)是获取接口实例实际的类型指针,以此调用实例所有可调用的方法,包括接口方法及自有方法。需要注意的是该写法必须与switch case联合使用,case中列出实现该接口的类型。
其实,就是看实现了ConstantInfo接口的实例,CONSTANT_Long_info和CONSTANT_Double_info结构表示8字节(long和double)的数值常量在class文件的常量池表中,所有的8字节常量均占两个表成员(项)的空间。就让索引加一。

getConstantInfo()方法按索引查找常量,代码如下:

getNameAndType()方法从常量池查找字段或方法的名字和描
述符,代码如下:

getClassName()方法从常量池查找类名,代码如下:

getUtf8()方法从常量池查找UTF-8字符串,代码如下:

ClassFileTest的常量池大小:

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

Java虚拟机规范一共定义了14种常量。
如下图:

详细解释:

整体代码
在classfile目录下创建constant_info.go文件,代码如下:
package classfile
// Constant pool tags tag常量值定义
const (
CONSTANT_Class = 7
CONSTANT_Fieldref = 9
CONSTANT_Methodref = 10
CONSTANT_InterfaceMethodref = 11
CONSTANT_String = 8
CONSTANT_Integer = 3
CONSTANT_Float = 4
CONSTANT_Long = 5
CONSTANT_Double = 6
CONSTANT_NameAndType = 12
CONSTANT_Utf8 = 1
CONSTANT_MethodHandle = 15
CONSTANT_MethodType = 16
CONSTANT_InvokeDynamic = 18
)
/*
cp_info {
u1 tag;
u1 info[];
}
*/
type ConstantInfo interface {
readInfo(reader *ClassReader)
}
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {
tag := reader.readUint8()
c := newConstantInfo(tag, cp)
c.readInfo(reader)
return c
}
// todo ugly code
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {
switch tag {
case CONSTANT_Integer:
return &ConstantIntegerInfo{}
case CONSTANT_Float:
return &ConstantFloatInfo{}
case CONSTANT_Long:
return &ConstantLongInfo{}
case CONSTANT_Double:
return &ConstantDoubleInfo{}
case CONSTANT_Utf8:
return &ConstantUtf8Info{}
case CONSTANT_String:
return &ConstantStringInfo{cp: cp}
case CONSTANT_Class:
return &ConstantClassInfo{cp: cp}
case CONSTANT_Fieldref:
return &ConstantFieldrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_Methodref:
return &ConstantMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_InterfaceMethodref:
return &ConstantInterfaceMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_NameAndType:
return &ConstantNameAndTypeInfo{}
case CONSTANT_MethodType:
return &ConstantMethodTypeInfo{}
case CONSTANT_MethodHandle:
return &ConstantMethodHandleInfo{}
case CONSTANT_InvokeDynamic:
return &ConstantInvokeDynamicInfo{}
default:
panic("java.lang.ClassFormatError: constant pool tag!")
}
}
定义ConstantInfo接口来表示常量信息:

readConstantInfo()函数先读出tag值,然后调用newConstantInfo()函数创建具体的常量,最后调用常量的readInfo()方法读取常量信息,

newConstantInfo()根据tag值创建具体的常量。

下面的小节详细介绍各种常量。
4.CONSTANT_Integer_info
CONSTANT_Integer_info使用4字节存储整数常量,其结构定义如下:

CONSTANT_Integer_info和后面将要介绍的其他三种数字常量无论是结构,还是实现,都非常相似,所以把它们定义在同一个文件中。在classfile目录下创建cp_numeric.go文件,
整体代码
package classfile
import "math"
/*
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
*/
type ConstantIntegerInfo struct {
val int32
}
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint32()
self.val = int32(bytes)
}
func (self *ConstantIntegerInfo) Value() int32 {
return self.val
}
代码解释
1.ConstantIntegerInfo结构体
ConstantIntegerInfo结构体,存储CONSTANT_Integer_info整数常量

2.readInfo()
readInfo()先读取一个uint32数据,然后把它转型成int32类型

3.Value()
Value()是Getter方法

CONSTANT_Integer_info正好可以容纳一个Java的int型常量,但实际上比int更小的boolean、byte、short和char类型的常量也放在CONSTANT_Integer_info中。编译器给ClassFileTest类的INT字段生成了一个CONSTANT_Integer_info常量,
如:SHORT字段

jclasslib

5.CONSTANT_Float_info
CONSTANT_Float_info使用4字节存储IEEE754单精度浮点数常量,结构如下:

整体代码
继续在cp_numeric.go文件中操作,代码如下:
/*
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
*/
type ConstantFloatInfo struct {
val float32
}
func (self *ConstantFloatInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint32()
self.val = math.Float32frombits(bytes)
}
func (self *ConstantFloatInfo) Value() float32 {
return self.val
}
代码解释
ConstantFloatInfo结构体
定义ConstantFloatInfo结构体,存储CONSTANT_Float_info单精度浮点数常量

readInfo
readInfo()先读取一个uint32数据,然后调用math包的Float32frombits()函数把它转换成float32类型。

math包的Float32frombits():

b要转换的32位二进制值,类型为uint32。Float32frombits()函数,该函数返回与IEEE 754二进制表示形式b对应的浮点数,其中b的符号位和结果位于相同的位位置。
package main
import (
"fmt"
"math"
)
func main() {
b := uint32(0x0044567)
f := math.Float32frombits(b)
fmt.Println(f)
}
Value()
Value()是Getter方法

编译器给ClassFileTest类的PI字段生成了一个CONSTANT_Float_info常量,如

用jclasslib

6.CONSTANT_Long_info
CONSTANT_Long_info使用8字节存储整数常量,结构如下:

整体代码
在cp_numeric.go文件中继续编辑
/*
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
*/
type ConstantLongInfo struct {
val int64
}
func (self *ConstantLongInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint64()
self.val = int64(bytes)
}
func (self *ConstantLongInfo) Value() int64 {
return self.val
}
代码解释
ConstantLongInfo结构体
定义ConstantLongInfo结构体,存储CONSTANT_Long_info

readInfo方法
readInfo()先读取一个uint64数据,然后把它转型成int64类型。

Value()
Value()是Getter方法

编译器给ClassFileTest类的LONG字段生成了一个CONSTANT_Long_info常量,


7. CONSTANT_Double_info
最后一个数字常量是CONSTANT_Double_info,使用8字节存储IEEE754双精度浮点数,结构如下:

整体代码
在cp_numeric.go文件中继续编辑
/*
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
*/
type ConstantDoubleInfo struct {
val float64
}
func (self *ConstantDoubleInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint64()
self.val = math.Float64frombits(bytes)
}
func (self *ConstantDoubleInfo) Value() float64 {
return self.val
}
代码解释
ConstantDoubleInfo结构体
定义ConstantDoubleInfo结构体

readInfo方法
readInfo()先读取一个uint64数据,然后调用math包的Float64frombits()函数把它转换成float64类型。Float64frombits与Float32frombits()函数用法差不多,仅差在数据类型上。不在阐述。

Value()
Value()是Getter方法

编译器给ClassFileTest类的E字段生成了一个CONSTANT_Double_info常量,


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

注意,字符串在class文件中是以MUTF-8(Modified UTF-8)方式编码的。而没有使用用标准的UTF-8编码方式。
可以参考我的博客:
JVM字符编码问题 – Dreams (tanjy.site)
整体代码
在classfile目录下创建cp_utf8.go文件,代码如下:
package classfile
import "fmt"
import "unicode/utf16"
/*
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
*/
type ConstantUtf8Info struct {
str string
}
func (self *ConstantUtf8Info) readInfo(reader *ClassReader) {
length := uint32(reader.readUint16())
bytes := reader.readBytes(length)
self.str = decodeMUTF8(bytes)
}
func (self *ConstantUtf8Info) Str() string {
return self.str
}
/*
func decodeMUTF8(bytes []byte) string {
return string(bytes) // not correct!
}
*/
// mutf8 -> utf16 -> utf32 -> string
// see java.io.DataInputStream.readUTF(DataInput)
func decodeMUTF8(bytearr []byte) string {
utflen := len(bytearr)
chararr := make([]uint16, utflen)
var c, char2, char3 uint16
count := 0
chararr_count := 0
for count < utflen {
c = uint16(bytearr[count])
if c > 127 {
break
}
count++
chararr[chararr_count] = c
chararr_count++
}
for count < utflen {
c = uint16(bytearr[count])
switch c >> 4 {
case 0, 1, 2, 3, 4, 5, 6, 7:
/* 0xxxxxxx*/
count++
chararr[chararr_count] = c
chararr_count++
case 12, 13:
/* 110x xxxx 10xx xxxx*/
count += 2
if count > utflen {
panic("malformed input: partial character at end")
}
char2 = uint16(bytearr[count-1])
if char2&0xC0 != 0x80 {
panic(fmt.Errorf("malformed input around byte %v", count))
}
chararr[chararr_count] = c&0x1F<<6 | char2&0x3F
chararr_count++
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx*/
count += 3
if count > utflen {
panic("malformed input: partial character at end")
}
char2 = uint16(bytearr[count-2])
char3 = uint16(bytearr[count-1])
if char2&0xC0 != 0x80 || char3&0xC0 != 0x80 {
panic(fmt.Errorf("malformed input around byte %v", (count - 1)))
}
chararr[chararr_count] = c&0x0F<<12 | char2&0x3F<<6 | char3&0x3F<<0
chararr_count++
default:
/* 10xx xxxx, 1111 xxxx */
panic(fmt.Errorf("malformed input around byte %v", count))
}
}
// The number of chars produced may be less than utflen
chararr = chararr[0:chararr_count]
runes := utf16.Decode(chararr)
return string(runes)
}
代码解释
ConstantUtf8Info结构体
在其中定义ConstantUtf8Info结构体

readInfo方法
readInfo()方法先读取出[]byte,然后调用decodeMUTF8()函数把它解码成Go字符串,代码如下:

Str()
Str()是Getter方法

decodeMUTF8方法
Java序列化机制也使用了MUTF-8编码。java.io.DataInput和java.io.DataOutput接口分别定义了readUTF()和writeUTF()方法,可以读写MUTF-8编码的字符串。因为Go语言字符串使用UTF-8编码,所以如果字符串中不包含null字符或补充字符。
decodeMUTF8()是《自己动手写Java虚拟机》的作者根据java.io.DataInputStream.readUTF()方法改写的,他并没有对代码进行解释。
所以我对java.io.DataInputStream.readUTF()方法的解释写了一篇博客,
readUTF()源码解析 – Dreams (tanjy.site)
与decodeMUTF8()方法一样的道理,不在阐述。
字段名、字段描述符等就是以字符串的形式存储在class文件中的,如字段PI对应的CONSTANT_Utf8_info常量

9.CONSTANT_String_info
CONSTANT_String_info常量表示java.lang.String字面量,结构如下:

可以看到,CONSTANT_String_info本身并不存放字符串数据,只存了常量池索引,这个索引指向一个CONSTANT_Utf8_info常量。
整体代码
在classfile目录下创建cp_string.go文件,代码如下:
package classfile
/*
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
*/
type ConstantStringInfo struct {
cp ConstantPool
stringIndex uint16
}
func (self *ConstantStringInfo) readInfo(reader *ClassReader) {
self.stringIndex = reader.readUint16()
}
func (self *ConstantStringInfo) String() string {
return self.cp.getUtf8(self.stringIndex)
}代码解释
ConstantStringInfo结构体
在其中定义ConstantStringInfo结构体

readInfo方法
readInfo()方法读取常量池索引,代码如下:

String()方法
String()方法按索引从常量池中查找字符串,代码如下:

ClassFileTest的main()方法使用了字符串字面量“Hello,World!”,对应的CONSTANT_String_info常量如图

可以看到,string_index是52(0x34)。我们按图索骥,从常量池中找出第52个常量,确实是个CONSTANT_Utf8_info,如图

10.CONSTANT_Class_info
CONSTANT_Class_info常量表示类或者接口的符号引用,结构如下:

和CONSTANT_String_info类似,name_index是常量池索引,指向CONSTANT_Utf8_info常量。
在classfile目录下创建cp_class.go文件,代码如下:
整体代码
package classfile
/*
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
*/
type ConstantClassInfo struct {
cp ConstantPool
nameIndex uint16
}
func (self *ConstantClassInfo) readInfo(reader *ClassReader) {
self.nameIndex = reader.readUint16()
}
func (self *ConstantClassInfo) Name() string {
return self.cp.getUtf8(self.nameIndex)
}
代码解释
ConstantClassInfo结构体
在其中定义ConstantClassInfo结构体

readInfo方法
readInfo()方法读取常量池索引,代码如下:

Name方法
Name()方法按索引从常量池中查找字符串,代码如下:

类和超类索引,以及接口表中的接口索引指向的都是CONSTANT_Class_info常量。由图


11.CONSTANT_NameAndType_info
CONSTANT_NameAndType_info给出字段或方法的名称和描述符。CONSTANT_Class_info和CONSTANT_NameAndType_info加在一起可以唯一确定一个字段或者方法。其结构如下:

字段或方法名由name_index给出,字段或方法的描述符由descriptor_index给出。name_index和descriptor_index都是常量池索引,指向CONSTANT_Utf8_info常量。字段和方法名就是代码中出
现的(或者编译器生成的)字段或方法的名字。Java虚拟机规范定义了一种简单的语法来描述字段和方法,可以根据下面的规则生成描述符。
1.类型描述符:
- 基本类型byte、short、char、int、long、float和double的描述符是单个字母,分别对应B、S、C、I、J、F和D。注意,long的描述符是J而不是L。
- 引用类型的描述符是L+类的完全限定名+分号。
- 数组类型的描述符是[+数组元素类型描述符。
2.字段描述符就是字段类型的描述符。
3.方法描述符是(分号分隔的参数类型描述符)+返回值类型描述符,其中void返回值由单个字母V表示。

更详细的介绍可以参考Java虚拟机规范4.3节。Chapter 4. The class File Format (oracle.com)
示例:

CONSTANT_NameAndType_info结构要同时包含名称和描述符的原因:Java语言支持方法重载(override),不同的方法可以有相同的名字,只要参数列表不同即可。
Java语法的限制Java是不能定义多个同名字段的,哪怕它们的类型各不相同,但class文件不限制。
整体代码
在classfile目录下创建cp_name_and_type.go文件,代码如下:
package classfile
/*
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
*/
type ConstantNameAndTypeInfo struct {
nameIndex uint16
descriptorIndex uint16
}
func (self *ConstantNameAndTypeInfo) readInfo(reader *ClassReader) {
self.nameIndex = reader.readUint16()
self.descriptorIndex = reader.readUint16()
}
代码解释
ConstantName-AndTypeInfo结构体
在其中定义ConstantName-AndTypeInfo结构体,

readInfo方法
readInfo()方法读取常量池索引,代码如下:

12.CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info
CONSTANT_Fieldref_info表示字段符号引用,CONSTANT_Methodref_info表示普通(非接口)方法符号引用,CONSTANT_InterfaceMethodref_info表示接口方法符号引用。

class_index和name_and_type_index都是常量池索引,分别指向CONSTANT_Class_info和CONSTANT_NameAndType_info常量。先定义一个统一的结构体ConstantMemberrefInfo来表示这3种常量。
整体代码
在classfile目录下创建cp_member_ref.go文件,把下面的代码输入进去。
package classfile
/*
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
*/
type ConstantFieldrefInfo struct{ ConstantMemberrefInfo }
type ConstantMethodrefInfo struct{ ConstantMemberrefInfo }
type ConstantInterfaceMethodrefInfo struct{ ConstantMemberrefInfo }
type ConstantMemberrefInfo struct {
cp ConstantPool
classIndex uint16
nameAndTypeIndex uint16
}
func (self *ConstantMemberrefInfo) readInfo(reader *ClassReader) {
self.classIndex = reader.readUint16()
self.nameAndTypeIndex = reader.readUint16()
}
func (self *ConstantMemberrefInfo) ClassName() string {
return self.cp.getClassName(self.classIndex)
}
func (self *ConstantMemberrefInfo) NameAndDescriptor() (string, string) {
return self.cp.getNameAndType(self.nameAndTypeIndex)
}
代码解释
继承
然后定义三个结构体“继承”ConstantMemberrefInfo。Go语言没有“继承”这个概念,但可以通过结构体嵌套来模拟

readInfo方法和Getter方法
readInfo()方法读取常量池索引:

ClassFileTest类的main()方法使用了java.lang.System类的out字段,该字段由常量池第2项指出,



回顾一下常量池表的结构
constant_pool [](常量池)
- constant_pool是一种表结构,以 1 ~ constant_pool_count -1为索引,表明了后面有多少个常量项。
- 常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
- 它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)。
- 对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。(CONSTANT_Integer_info)

字面量和符号引用
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。如下表:

13.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)



