自己动手写 Java 虚拟机笔记 - 第六部分:类与对象的核心实现

自己动手写 Java 虚拟机笔记 - 第六部分:类与对象的核心实现

Luca Ju
2025-06-20 / 0 评论 / 4 阅读 / 正在检测是否收录...

前言

在前一章中,我们实现了指令集和基础解释器,让 JVM 能够执行简单的字节码逻辑。本章将聚焦 JVM 中类与对象的核心机制—— 包括类信息的存储(方法区)、类加载过程、对象创建与访问,以及符号引用解析等关键逻辑,这是 JVM 实现 “面向对象” 特性的基础。

参考资料

《自己动手写 Java 虚拟机》—— 张秀宏

开发环境

工具 / 环境版本说明
操作系统MacOS 15.5基于 Intel/Apple Silicon 均可
JDK1.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 索引);
  • 方法的 maxStackcode 等信息来自 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
}

类加载核心步骤

  1. 加载:读取 Class 文件字节流,解析为 Class 实例;
  2. 链接

    • 验证:确保 Class 文件格式合法;
    • 准备:为静态变量分配内存并设置默认值(如 int 默认为 0);
    • 解析:将符号引用转化为直接引用(如将类符号引用指向已加载的类);
  3. 初始化:执行类构造器 <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 数组存储实例变量,每个字段对应一个槽位(longdouble 占 2 个槽位),槽位索引由 Field.slotId 确定。

2. 类变量与实例变量的区别

类型存储位置生命周期访问方式示例
实例变量对象的 fields与对象一致通过对象引用访问obj.name
类变量类的 staticVars与类一致通过类名或对象引用访问Class.countobj.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(包含静态字段和实例字段,简单逻辑用于验证);
  • 测试步骤:

    1. 配置类路径,加载 MyObject 类;
    2. 查找 main 方法并通过解释器执行;
    3. 验证类加载、对象创建及字段访问是否正确。

2. 测试结果

执行命令 go install ./ch06/ && ch06 Myobject 后,日志显示类成功加载,且对象创建、字段赋值等操作正常执行:

ch06test.png

结论:方法区类信息存储、类加载流程、对象创建及 NEW/PUT_STATIC 等指令执行正确。

本章小结

本章实现了 JVM 中类与对象的核心机制,包括:

  1. 方法区设计:通过 Class 结构体存储类元信息(字段、方法、常量池等);
  2. 运行时常量池:管理字面量和符号引用,支持类与成员的关联;
  3. 类加载器:完成类的加载、链接和初始化,将类信息载入方法区;
  4. 对象模型:通过 Object 结构体存储实例变量,区分实例变量与类变量;
  5. 核心指令:实现 NEW(创建对象)、PUT_STATIC(设置静态字段)等指令,支持类与对象的操作。

下一章将完善方法调用机制(如 invokevirtual 指令)和继承关系处理,让 JVM 支持更复杂的面向对象特性。

源码地址:https://github.com/Jucunqi/jvmgo.git
0

评论 (0)

取消