代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.运行时常量池概述
- 运行时常量池(Runtime Constant Pool) 是方法区的一部分。
- 常量池表 (Constant Pool Table) 是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
- JVM为每个已加载的类型(类或接口) 都维护一个常量池。池中的数据项像数组项一样是通过索引访问的。
- 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
- 运行时常量池,相对于class文件常量池的另一重要特征是:具备动态性,比如:String. intern() 方法,String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。
- 运行时常量池类似于传统编程语言中的符号表(symbo table),但是它所包含的数据却比符号表要更加丰富一些。
- 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutofMemoryError异常。
运行时常量池主要存放两类信息:字面量(literal)和符号引用(symbolic reference)。字面量包括整数、浮点数和字符串字面量;符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。
2.全部代码
在rtda\heap目录下创建constant_pool.go文件,在其中定义Constant接口和ConstantPool结构体,代码如下:
package heap
import (
"fmt"
"jvmgo/ch06/classfile"
)
type Constant interface{}
type ConstantPool struct {
class *Class
consts []Constant
}
func newConstantPool(class *Class, cfCp classfile.ConstantPool) *ConstantPool {
cpCount := len(cfCp)
consts := make([]Constant, cpCount)
rtCp := &ConstantPool{class, consts}
for i := 1; i < cpCount; i++ {
cpInfo := cfCp[i]
switch cpInfo.(type) {
case *classfile.ConstantIntegerInfo:
intInfo := cpInfo.(*classfile.ConstantIntegerInfo)
consts[i] = intInfo.Value()
case *classfile.ConstantFloatInfo:
floatInfo := cpInfo.(*classfile.ConstantFloatInfo)
consts[i] = floatInfo.Value()
case *classfile.ConstantLongInfo:
longInfo := cpInfo.(*classfile.ConstantLongInfo)
consts[i] = longInfo.Value()
i++
case *classfile.ConstantDoubleInfo:
doubleInfo := cpInfo.(*classfile.ConstantDoubleInfo)
consts[i] = doubleInfo.Value()
i++
case *classfile.ConstantStringInfo:
stringInfo := cpInfo.(*classfile.ConstantStringInfo)
consts[i] = stringInfo.String()
case *classfile.ConstantClassInfo:
classInfo := cpInfo.(*classfile.ConstantClassInfo)
consts[i] = newClassRef(rtCp, classInfo)
case *classfile.ConstantFieldrefInfo:
fieldrefInfo := cpInfo.(*classfile.ConstantFieldrefInfo)
consts[i] = newFieldRef(rtCp, fieldrefInfo)
case *classfile.ConstantMethodrefInfo:
methodrefInfo := cpInfo.(*classfile.ConstantMethodrefInfo)
consts[i] = newMethodRef(rtCp, methodrefInfo)
case *classfile.ConstantInterfaceMethodrefInfo:
methodrefInfo := cpInfo.(*classfile.ConstantInterfaceMethodrefInfo)
consts[i] = newInterfaceMethodRef(rtCp, methodrefInfo)
default:
// todo
}
}
return rtCp
}
func (self *ConstantPool) GetConstant(index uint) Constant {
if c := self.consts[index]; c != nil {
return c
}
panic(fmt.Sprintf("No constants at index %d", index))
}
3.代码解释
GetConstant方法
GetConstant()方法根据索引返回常量,代码如下:

newConstantPool函数
newConstantPool()函数把class文件中的常量池转换成运行时常量池。代码都在上面了,就不截全图了,核心逻辑就是把[]classfile.ConstantInfo转换成[]heap.Constant。具体常量的转换
在switch-case中,最简单的是int或float型常量,直接取出常量值,放进consts中即可。

如果是long或double型常量,也是直接提取常量值放进consts中。因为他们在常量池中占据两个位置,所以索引+1,

如果是字符串常量,变成Go语言字符串,放入consts中,代码如图:

还剩下4种类型的常量需要处理,分别是类、字段、方法和接口方法的符号引用。下面说

基本类型常量的使用在6.4节。
3.类符号引用
因为4种类型的符号引用有一些共性,所以仍然使用继承来减少重复代码。
全部代码
在\rtda\heap目录下创建cp_symref.go文件,在其中定义SymRef结构体,代码如下:
package heap
// symbolic reference
type SymRef struct {
cp *ConstantPool
className string
class *Class
}
cp字段存放符号引用所在的运行时常量池指针,这样就可以通过符号引用访问到运行时常量池,进一步又可以访问到类数据。className字段存放类的完全限定名。class字段缓存解析后的类结
构体指针,这样类符号引用只需要解析一次就可以了,后续可以直接使用缓存值。对于类符号引用,只要有类名,就可以解析符号引用。对于字段,首先要解析类符号引用得到类数据,然后用字段名和描述符查找字段数据。方法符号引用的解析过程和字段符号引用类似。

接下来在\rtda\heap目录下创建cp_classref.go文件,在其中定义ClassRef结构体,代码如下:
package heap
import "jvmgo/ch06/classfile"
type ClassRef struct {
SymRef
}
func newClassRef(cp *ConstantPool, classInfo *classfile.ConstantClassInfo) *ClassRef {
ref := &ClassRef{}
ref.cp = cp
ref.className = classInfo.Name()
return ref
}
ClassRef继承了SymRef,但是并没有添加任何字段。newClassRef()函数根据class文件中存储的类常量创建ClassRef实例。

4.字段符号引用
定义MemberRef结构体来存放字段和方法符号引用共有的信息。
在\rtda\heap目录下创建cp_memberref.go文件,在其中定义MemberRef结构体,代码如下:
package heap
import "jvmgo/ch06/classfile"
type MemberRef struct {
SymRef
name string
descriptor string
}
func (self *MemberRef) copyMemberRefInfo(refInfo *classfile.ConstantMemberrefInfo) {
self.className = refInfo.ClassName()
self.name, self.descriptor = refInfo.NameAndDescriptor()
}
func (self *MemberRef) Name() string {
return self.name
}
func (self *MemberRef) Descriptor() string {
return self.descriptor
}
在Java中,我们并不能在同一个类中定义名字相同,但类型不同的两个字段,但是这只是Java语言的限制,而不是Java虚拟机规范的限制。所以字段符号引用还要存放字段描述符呢。站在Java虚拟机的角度,一个类是完全可以有多个同名字段的,只要它们的类型互不相同就可以。
copyMemberRefInfo()方法从class文件内存储的字段或方法常量中提取数据,代码如下:

Getter方法

接下来在\rtda\heap目录下创建cp_fieldref.go文件,在其中定义FieldRef结构体,代码如下:
package heap
import "jvmgo/ch06/classfile"
type FieldRef struct {
MemberRef
field *Field
}
func newFieldRef(cp *ConstantPool, refInfo *classfile.ConstantFieldrefInfo) *FieldRef {
ref := &FieldRef{}
ref.cp = cp
ref.copyMemberRefInfo(&refInfo.ConstantMemberrefInfo)
return ref
}
field字段缓存解析后的字段指针,newFieldRef()方法创建FieldRef实例,:

5.方法符号引用
在\rtda\heap目录下创建cp_methodref.go文件,在其中定义MethodRef结构体,代码如下:
package heap
import "jvmgo/ch06/classfile"
type MethodRef struct {
MemberRef
method *Method
}
func newMethodRef(cp *ConstantPool, refInfo *classfile.ConstantMethodrefInfo) *MethodRef {
ref := &MethodRef{}
ref.cp = cp
ref.copyMemberRefInfo(&refInfo.ConstantMemberrefInfo)
return ref
}代码简单,不多解释了。
6.接口方法符号引用
在\rtda\heap目录下创建cp_interface_methodref.go文件,在其中定义Interface-MethodRef结构体,代码如下:
package heap
import "jvmgo/ch06/classfile"
type InterfaceMethodRef struct {
MemberRef
method *Method
}
func newInterfaceMethodRef(cp *ConstantPool, refInfo *classfile.ConstantInterfaceMethodrefInfo) *InterfaceMethodRef {
ref := &InterfaceMethodRef{}
ref.cp = cp
ref.copyMemberRefInfo(&refInfo.ConstantMemberrefInfo)
return ref
}
代码和前面差不多,也不多解释了。接口方法符号引用的解析同样会在第7章详细介绍。到此为止。
所有的符号引用都已经定义好了,它们的继承结构如图

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


