前言
在前一章中,我们实现了指令集和基础解释器,让 JVM 能够执行简单的字节码逻辑。本章将聚焦 JVM 中类与对象的核心机制—— 包括类信息的存储(方法区)、类加载过程、对象创建与访问,以及符号引用解析等关键逻辑,这是 JVM 实现 “面向对象” 特性的基础。
参考资料
《自己动手写 Java 虚拟机》—— 张秀宏
开发环境
| 工具 / 环境 | 版本 | 说明 |
|---|---|---|
| 操作系统 | MacOS 15.5 | 基于 Intel/Apple Silicon 均可 |
| JDK | 1.8 | 用于字节码分析和测试 |
| Go 语言 | 1.23.10 | 项目开发主语言 |
第六章:类与对象的核心实现
类和对象是 Java 面向对象编程的核心,JVM 需要通过特定的数据结构存储类信息、管理对象生命周期,并提供指令支持类与对象的操作(如创建对象、访问字段等)。本章将实现这些核心机制。
一、方法区:类信息的 “仓库”
方法区是 JVM 中线程共享的内存区域,用于存储已加载类的元信息(类结构、字段、方法、常量等),是类加载后的数据载体。
1. 类信息的核心结构(Class 结构体)
Class 结构体整合了从 Class 文件解析的所有类信息,是方法区存储的核心单元:
// Class 存储类的完整信息(对应方法区中的类元数据)
type Class struct {
accessFlags uint16 // 类访问标志(如 public、final、abstract)
name string // 类的全限定名(如 "java/lang/String")
superClassName string // 父类全限定名(如 "java/lang/Object")
interfaceNames []string // 实现的接口全限定名列表
constantPool *ConstantPool // 运行时常量池(类关联的常量)
fields []*Field // 类的字段列表
methods []*Method // 类的方法列表
loader *ClassLoader // 加载该类的类加载器
superClass *Class // 父类的 Class 实例(运行时关联)
interfaces []*Class // 实现的接口 Class 实例列表(运行时关联)
instanceSlotCount uint // 实例字段占用的槽位总数(用于对象内存分配)
staticSlotCount uint // 静态字段占用的槽位总数(用于方法区存储)
staticVars Slots // 静态变量(存储静态字段的实际值)
}
// newClass 从 Class 文件解析结果创建 Class 实例
func newClass(cf *classfile.ClassFile) *Class {
class := &Class{}
class.accessFlags = cf.AccessFlags()
class.name = cf.ClassName()
class.superClassName = cf.SuperClassName()
class.interfaceNames = cf.InterfaceNames()
// 初始化常量池、字段、方法
class.constantPool = newConstantPool(class, cf.ConstantPool())
class.fields = newFields(class, cf.Fields())
class.methods = newMethod(class, cf.Methods())
return class
}核心字段说明:
constantPool:存储类的运行时常量池(字面量和符号引用);fields/methods:存储类的字段和方法信息(从 Class 文件的字段表、方法表解析);staticVars:存储静态字段的实际值(静态变量属于类,而非对象,因此存放在方法区)。
2. 字段与方法的结构
字段(Field)和方法(Method)是类的核心成员,通过复用 ClassMember 结构体统一管理公共属性:
// ClassMember 字段和方法的公共属性
type ClassMember struct {
accessFlags uint16 // 成员访问标志(如 public、private、static)
name string // 成员名称(如字段名 "age"、方法名 "toString")
descriptor string // 成员描述符(如字段 "I" 表示 int,方法 "(I)V" 表示入参 int、返回 void)
class *Class // 所属的类
}
// Field 类的字段信息
type Field struct {
ClassMember // 继承公共属性
constValueIndex uint // 常量值索引(若字段被 final 修饰,指向常量池中的值)
slotId uint // 字段在槽位中的索引(实例字段在对象中,静态字段在方法区)
}
// Method 类的方法信息
type Method struct {
ClassMember // 继承公共属性
maxStack uint // 操作数栈最大深度(从 Code 属性获取)
maxLocals uint // 局部变量表大小(从 Code 属性获取)
code []byte // 方法的字节码指令(从 Code 属性获取)
}关键设计:
- 字段和方法通过
slotId定位存储位置(实例字段的slotId用于对象的槽位索引,静态字段的slotId用于staticVars索引); - 方法的
maxStack和code等信息来自 Class 文件的Code属性,是解释器执行的基础。
二、运行时常量池:常量与符号引用的 “容器”
运行时常量池是方法区的一部分,存储类加载时从常量池解析的字面量(如整数、字符串)和符号引用(如类、字段、方法的引用),是类与其他结构关联的桥梁。
1. 常量池结构
// Constant 常量接口(所有常量类型都需实现)
type Constant interface{}
// ConstantPool 运行时常量池
type ConstantPool struct {
class *Class // 所属的类
consts []Constant // 常量数组(存储字面量和符号引用)
}2. 符号引用的核心类型
符号引用是编译期的概念,用于指代尚未加载的类、字段或方法,在运行时通过解析转化为直接引用(内存地址)。核心符号引用类型如下:
| 符号引用类型 | 作用 | 核心结构 |
|---|---|---|
ClassRef | 类的符号引用 | 继承 SymRef,包含类名和所属常量池 |
FieldRef | 字段的符号引用 | 继承 MemberRef,包含字段名、描述符及所属类引用 |
MethodRef | 类方法的符号引用 | 继承 MemberRef,包含方法名、描述符及所属类引用 |
InterfaceMethodref | 接口方法的符号引用 | 继承 MemberRef,逻辑与 MethodRef 类似 |
示例:类符号引用与成员符号引用
// SymRef 所有符号引用的基类
type SymRef struct {
cp *ConstantPool // 所属常量池
className string // 目标类名(如 "java/lang/Object")
class *Class // 解析后的目标类(直接引用,运行时赋值)
}
// MemberRef 字段和方法符号引用的基类
type MemberRef struct {
SymRef // 继承类符号引用的属性
name string // 成员名称(如字段名 "name"、方法名 "getAge")
descriptor string // 成员描述符(如字段 "Ljava/lang/String;"、方法 "()I")
}
// ClassRef 类符号引用
type ClassRef struct {
SymRef // 仅需继承类符号引用的基础属性
}
// FieldRef 字段符号引用
type FieldRef struct {
MemberRef // 继承成员引用属性
field *Field // 解析后的字段(直接引用,运行时赋值)
}三、类加载器:类的 “加载器” 与 “入口”
类加载器负责将 Class 文件加载到 JVM 中,通过 “加载→链接→初始化” 过程将类信息存入方法区,是类进入运行时的关键组件。
1. 类加载器核心逻辑
// ClassLoader 类加载器
type ClassLoader struct {
cp *classpath.Classpath // 类路径(用于查找 Class 文件)
classMap map[string]*Class // 已加载类的缓存(类名→Class 实例)
}
// LoadClass 加载指定类(核心入口方法)
func (c *ClassLoader) LoadClass(name string) *Class {
// 1. 检查缓存,若已加载则直接返回
if class, ok := c.classMap[name]; ok {
return class
}
// 2. 未加载则加载非数组类(数组类由 JVM 直接创建,此处简化)
return c.loadNonArrayClass(name)
}
// loadNonArrayClass 加载非数组类的完整流程
func (c *ClassLoader) loadNonArrayClass(name string) *Class {
// 步骤 1:读取 Class 文件字节流
data, entry := c.readClass(name)
// 步骤 2:解析字节流为 Class 实例(加载阶段)
class := c.defineClass(data)
// 步骤 3:链接(验证、准备、解析)
link(class)
fmt.Printf("[Loaded %s from %s]\n", name, entry)
return class
}类加载核心步骤:
- 加载:读取 Class 文件字节流,解析为
Class实例; 链接
:
- 验证:确保 Class 文件格式合法;
- 准备:为静态变量分配内存并设置默认值(如 int 默认为 0);
- 解析:将符号引用转化为直接引用(如将类符号引用指向已加载的类);
- 初始化:执行类构造器
<clinit>()方法(初始化静态变量和静态代码块)。
四、对象、实例变量与类变量
对象是类的实例,存储实例变量;类变量(静态变量)属于类,存储在方法区。JVM 通过槽位(Slots)管理变量的存储。
1. 对象的结构
// Object 表示 JVM 中的对象实例
type Object struct {
class *Class // 对象所属的类
fields Slots // 实例变量(存储对象的字段值,大小由类的 instanceSlotCount 决定)
}
// newObject 创建对象实例
func newObject(c *Class) *Object {
return &Object{
class: c,
fields: newSlots(c.instanceSlotCount), // 按实例字段数量分配槽位
}
}说明:对象的 fields 数组存储实例变量,每个字段对应一个槽位(long 和 double 占 2 个槽位),槽位索引由 Field.slotId 确定。
2. 类变量与实例变量的区别
| 类型 | 存储位置 | 生命周期 | 访问方式 | 示例 |
|---|---|---|---|---|
| 实例变量 | 对象的 fields | 与对象一致 | 通过对象引用访问 | obj.name |
| 类变量 | 类的 staticVars | 与类一致 | 通过类名或对象引用访问 | Class.count 或 obj.count |
五、符号引用解析:从 “符号” 到 “实际地址”
符号引用是编译期的间接引用,需在运行时解析为直接引用(内存中的实际对象 / 字段 / 方法)才能使用。
1. 类符号引用解析
// ResolveClass 将类符号引用解析为直接引用(Class 实例)
func (s *SymRef) ResolveClass() *Class {
if s.class == nil {
s.resolveClassRef() // 未解析则执行解析
}
return s.class
}
// resolveClassRef 实际执行类解析逻辑
func (s *SymRef) resolveClassRef() {
d := s.cp.class // 引用所在的类(当前类)
c := d.loader.LoadClass(s.className) // 加载目标类
// 检查访问权限(如当前类是否能访问目标类)
if !c.isAccessibleTo(d) {
panic("java.lang.IllegalAccessError")
}
s.class = c // 解析结果赋值给符号引用
}2. 字段符号引用解析
// ResolvedField 将字段符号引用解析为直接引用(Field 实例)
func (r *FieldRef) ResolvedField() *Field {
if r.field == nil {
r.resolveFieldRef() // 未解析则执行解析
}
return r.field
}
// resolveFieldRef 实际执行字段解析逻辑
func (r *FieldRef) resolveFieldRef() {
d := r.cp.class // 当前类
c := r.ResolveClass() // 解析字段所属的类
// 在类及其父类、接口中查找匹配的字段
field := lookupField(c, r.name, r.descriptor)
if field == nil {
panic("java.lang.NoSuchFieldError")
}
// 检查访问权限
if !field.isAccessibleTo(d) {
panic("java.lang.IllegalAccessError")
}
r.field = field // 解析结果赋值
}
// lookupField 在类、父类、接口中查找字段
func lookupField(c *Class, name string, descriptor string) *Field {
// 1. 在当前类中查找
for _, field := range c.fields {
if field.name == name && field.descriptor == descriptor {
return field
}
}
// 2. 在父类中递归查找
if c.superClass != nil {
return lookupField(c.superClass, name, descriptor)
}
// 3. 在接口中查找
for _, iface := range c.interfaces {
if field := lookupField(iface, name, descriptor); field != nil {
return field
}
}
return nil
}六、类与对象相关指令实现
JVM 提供专门的指令支持类与对象的操作,如创建对象、访问字段等。以下是核心指令的实现。
1. NEW 指令:创建对象
// NEW 创建类的实例对象
type NEW struct {
base.Index16Instruction // 包含常量池索引(指向类符号引用)
}
func (n *NEW) Execute(frame *rtda.Frame) {
cp := frame.Method().Class().ConstantPool()
// 1. 从常量池获取类符号引用
classRef := cp.GetConstant(n.Index).(*ClassRef)
// 2. 解析类符号引用,获取 Class 实例
class := classRef.ResolveClass()
// 3. 检查类是否可实例化(非接口、非抽象类)
if class.IsInterface() || class.IsAbstract() {
panic("java.lang.InstantiationError")
}
// 4. 创建对象实例并推送至操作数栈
ref := class.NewObject()
frame.OperandStack().PushRef(ref)
}
// NewObject 创建类的实例对象
func (c *Class) NewObject() *Object {
return newObject(c) // 内部调用 newObject 分配对象内存
}2. PUT_STATIC 指令:设置静态字段值
// PUT_STATIC 设置静态字段的值
func (p *PUT_STATIC) Execute(frame *rtda.Frame) {
currentMethod := frame.Method()
currentClass := currentMethod.Class()
cp := currentClass.ConstantPool()
// 1. 从常量池获取字段符号引用并解析
fieldRef := cp.GetConstant(p.Index).(*FieldRef)
field := fieldRef.ResolvedField()
class := field.Class()
// 2. 检查字段合法性(必须是静态字段)
if !field.IsStatic() {
panic("java.lang.IncompatibleClassChangeError")
}
// 检查 final 字段的赋值权限(只能在 <clinit> 方法中赋值)
if field.IsFinal() {
if currentClass != class || currentMethod.Name() != "<clinit>" {
panic("java.lang.IllegalAccessError")
}
}
// 3. 根据字段类型从操作数栈弹出值,存入静态变量
descriptor := field.Descriptor()
slotId := field.SlotId()
slots := class.StaticVars() // 静态变量存储在类的 staticVars 中
stack := frame.OperandStack()
// 根据字段描述符处理不同类型(int/float/long/double/引用)
switch descriptor[0] {
case 'Z', 'B', 'C', 'S', 'I': // 基本类型(boolean/byte/char/short/int)
slots.SetInt(slotId, stack.PopInt())
case 'F': // float
slots.SetFloat(slotId, stack.PopFloat())
case 'J': // long
slots.SetLong(slotId, stack.PopLong())
case 'D': // double
slots.SetDouble(slotId, stack.PopDouble())
case 'L', '[': // 引用类型(对象或数组)
slots.SetRef(slotId, stack.PopRef())
}
}七、功能测试:验证类加载与对象创建
通过加载自定义类并执行对象创建、字段访问逻辑,验证类与对象机制的正确性。
1. 测试代码与执行流程
- 测试类:
MyObject(包含静态字段和实例字段,简单逻辑用于验证); 测试步骤:
- 配置类路径,加载
MyObject类; - 查找
main方法并通过解释器执行; - 验证类加载、对象创建及字段访问是否正确。
- 配置类路径,加载
2. 测试结果
执行命令 go install ./ch06/ && ch06 Myobject 后,日志显示类成功加载,且对象创建、字段赋值等操作正常执行:

结论:方法区类信息存储、类加载流程、对象创建及 NEW/PUT_STATIC 等指令执行正确。
本章小结
本章实现了 JVM 中类与对象的核心机制,包括:
- 方法区设计:通过
Class结构体存储类元信息(字段、方法、常量池等); - 运行时常量池:管理字面量和符号引用,支持类与成员的关联;
- 类加载器:完成类的加载、链接和初始化,将类信息载入方法区;
- 对象模型:通过
Object结构体存储实例变量,区分实例变量与类变量; - 核心指令:实现
NEW(创建对象)、PUT_STATIC(设置静态字段)等指令,支持类与对象的操作。
下一章将完善方法调用机制(如 invokevirtual 指令)和继承关系处理,让 JVM 支持更复杂的面向对象特性。
源码地址:https://github.com/Jucunqi/jvmgo.git
评论 (0)