自己动手写 Java 虚拟机笔记 - 第八部分:数组与字符串的实现

自己动手写 Java 虚拟机笔记 - 第八部分:数组与字符串的实现

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

前言

在前一章中,我们实现了方法调用与返回机制,支撑了函数执行的核心流程。本章将聚焦 JVM 中数组和字符串的实现—— 这两类数据结构在 Java 中使用频繁,但它们的创建、存储和操作逻辑与普通对象存在显著差异。数组类由 JVM 运行时动态生成,而非从 Class 文件加载;字符串则通过常量池和字符串池实现共享。本章将详细实现这些特性,完善 JVM 对复杂数据结构的支持。

参考资料

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

开发环境

工具 / 环境版本说明
操作系统MacOS 15.5基于 Intel/Apple Silicon 均可
JDK1.8用于字节码分析和测试
Go 语言1.23.10项目开发主语言

第八章:数组与字符串的核心实现

数组和字符串是 Java 中最基础的数据结构,但其底层实现逻辑与普通对象不同。数组类由 JVM 动态生成,支持多维度和多种数据类型;字符串则通过常量池和字符串池实现高效存储和共享。本章将从数据结构设计、指令实现到功能测试,完整覆盖这两类结构的核心机制。

一、数组概述:与普通类的本质区别

数组是一种特殊的引用类型,其类信息并非来自 Class 文件,而是由 JVM 在运行时动态创建。理解数组与普通类的区别是实现的基础。

特性普通类数组类
类信息来源从 Class 文件加载由 JVM 运行时动态生成
创建指令new 指令 + 构造器初始化newarray/anewarray/multianewarray 指令
类名格式全限定名(如 java/lang/String特殊格式(如 [I 表示 int 数组,[[Ljava/lang/Object; 表示二维对象数组)
继承关系显式继承父类隐式继承 java/lang/Object,实现 CloneableSerializable 接口

核心差异:数组类的结构由 JVM 动态定义,无需预编译的 Class 文件;其创建和操作依赖专门的指令,而非普通对象的 new 指令和构造器。

二、数组的核心实现

1. 数组对象的数据结构

数组对象仍复用 Object 结构体,但通过 interface{} 字段存储数组元素(支持不同类型的数组数据):

// Object 统一表示普通对象和数组对象
type Object struct {
    class *Class       // 所属的类(数组类或普通类)
    data  interface{}  // 存储数据:普通对象存字段槽位,数组存元素集合
}

设计说明

  • 对于普通对象,data 字段存储实例变量的槽位数组(Slots);
  • 对于数组对象,data 字段存储 Go 切片(如 []int32 对应 int 数组,[]*Object 对应对象数组),通过 interface{} 兼容不同类型的数组元素。

2. 数组类的动态生成

数组类由 JVM 动态创建,无需加载 Class 文件。其类信息(如名称、继承关系)由 JVM 按固定规则生成:

// NewArray 创建数组对象(根据数组类和长度初始化元素)
func (c *Class) NewArray(count uint) *Object {
    if !c.IsArray() {
        panic("Not array class: " + c.name) // 校验是否为数组类
    }
    // 根据数组类名创建对应类型的 Go 切片(映射 Java 数组类型)
    switch c.Name() {
    case "[Z":  // boolean 数组
        return &Object{class: c, data: make([]int8, count)}  // boolean 用 int8 存储
    case "[B":  // byte 数组
        return &Object{class: c, data: make([]int8, count)}
    case "[C":  // char 数组
        return &Object{class: c, data: make([]uint16, count)} // char 用 uint16 存储
    case "[S":  // short 数组
        return &Object{class: c, data: make([]int16, count)}
    case "[I":  // int 数组
        return &Object{class: c, data: make([]int32, count)}
    case "[J":  // long 数组
        return &Object{class: c, data: make([]int64, count)}
    case "[F":  // float 数组
        return &Object{class: c, data: make([]float32, count)}
    case "[D":  // double 数组
        return &Object{class: c, data: make([]float64, count)}
    default:    // 对象数组(如 [Ljava/lang/Object;)
        return &Object{class: c, data: make([]*Object, count)}
    }
}

类型映射规则:Java 数组类型与 Go 切片类型的映射需严格对应,确保元素存储和操作的正确性(如 boolean 数组在 JVM 中实际用 byte 存储,故映射为 []int8)。

3. 数组类的加载逻辑

数组类的加载由类加载器特殊处理,无需读取 Class 文件,直接动态生成类信息:

// LoadClass 加载类(支持普通类和数组类)
func (c *ClassLoader) LoadClass(name string) *Class {
    // 1. 检查缓存,已加载则直接返回
    if class, ok := c.classMap[name]; ok {
        return class
    }
    // 2. 若为数组类,动态生成类信息
    if name[0] == '[' {
        return c.loadArrayClass(name)
    }
    // 3. 加载普通类(从 Class 文件读取)
    return c.loadNonArrayClass(name)
}

// loadArrayClass 动态生成数组类信息
func (c *ClassLoader) loadArrayClass(name string) *Class {
    // 构建数组类的基本信息
    class := &Class{
        accessFlags: ACC_PUBLIC,                  // 数组类默认为 public
        name:        name,                        // 数组类名(如 "[I")
        loader:      c,                           // 类加载器
        initStarted: true,                        // 数组类无需初始化
        superClass:  c.LoadClass("java/lang/Object"), // 继承 Object
        interfaces: []*Class{                     // 实现 Cloneable 和 Serializable 接口
            c.LoadClass("java/lang/Cloneable"),
            c.LoadClass("java/io/Serializable"),
        },
    }
    c.classMap[name] = class // 存入缓存
    return class
}

关键逻辑:数组类的继承和接口实现是固定的(继承 Object,实现 CloneableSerializable),无需像普通类那样从 Class 文件解析。

三、数组操作指令实现

JVM 提供专门的指令用于数组的创建、长度获取和元素访问,以下是核心指令的实现。

1. newarray:创建基本类型数组

用于创建基本类型的一维数组(如 int[]float[]),操作数包括基本类型标识和数组长度。

// 基本类型与 atype 对应关系(JVM 规范定义)
const (
    AT_BOOLEAN = 4  // boolean 数组
    AT_CHAR    = 5  // char 数组
    AT_FLOAT   = 6  // float 数组
    AT_DOUBLE  = 7  // double 数组
    AT_BYTE    = 8  // byte 数组
    AT_SHORT   = 9  // short 数组
    AT_INT     = 10 // int 数组
    AT_LONG    = 11 // long 数组
)

// NEW_ARRAY 创建基本类型数组
type NEW_ARRAY struct {
    atype uint8 // 基本类型标识(对应上述常量)
}

// 从字节码读取 atype 操作数
func (n *NEW_ARRAY) FetchOperands(reader *base.BytecodeReader) {
    n.atype = reader.ReadUint8()
}

// 执行指令:创建数组并推送引用到操作数栈
func (n *NEW_ARRAY) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    // 1. 从操作数栈弹出数组长度(必须非负)
    count := stack.PopInt()
    if count < 0 {
        panic("java.lang.NegativeArraySizeException")
    }
    // 2. 获取类加载器,解析数组类
    classLoader := frame.Method().Class().Loader()
    arrClass := getPrimitiveArrayClass(classLoader, n.atype)
    // 3. 创建数组对象并推送引用到栈顶
    arr := arrClass.NewArray(uint(count))
    stack.PushRef(arr)
}

// 根据 atype 获取对应的数组类
func getPrimitiveArrayClass(loader *heap.ClassLoader, atype uint8) *heap.Class {
    switch atype {
    case AT_BOOLEAN:
        return loader.LoadClass("[Z") // boolean 数组类名为 "[Z"
    case AT_BYTE:
        return loader.LoadClass("[B") // byte 数组类名为 "[B"
    // 省略其他类型映射...
    default:
        panic("Invalid atype!")
    }
}

执行流程

  1. 从操作数栈获取数组长度并校验非负;
  2. 根据 atype 确定数组类型(如 AT_INT 对应 [I 类);
  3. 创建数组对象并将引用推送回操作数栈。

2. anewarray:创建引用类型数组

用于创建引用类型的一维数组(如 String[]Object[]),操作数包括类符号引用索引和数组长度。

// ANEW_ARRAY 创建引用类型数组
type ANEW_ARRAY struct {
    base.Index16Instruction // 包含常量池索引(指向类符号引用)
}

func (a *ANEW_ARRAY) Execute(frame *rtda.Frame) {
    cp := frame.Method().Class().ConstantPool()
    // 1. 解析类符号引用,获取元素类型
    classRef := cp.GetConstant(a.Index).(*heap.ClassRef)
    componentClass := classRef.ResolveClass() // 如 "java/lang/String"
    // 2. 从操作数栈弹出数组长度并校验
    stack := frame.OperandStack()
    count := stack.PopInt()
    if count < 0 {
        panic("java.lang.NegativeArraySizeException")
    }
    // 3. 获取数组类(元素类型的数组类,如 "[Ljava/lang/String;")
    arrClass := componentClass.ArrayClass()
    // 4. 创建数组对象并推送引用
    arr := arrClass.NewArray(uint(count))
    stack.PushRef(arr)
}

// ArrayClass 获取元素类型对应的数组类
func (c *Class) ArrayClass() *Class {
    arrClassName := "[" + c.name // 数组类名规则:元素类名前加 "["
    return c.loader.LoadClass(arrClassName)
}

关键区别:与 newarray 不同,anewarray 需要先解析类符号引用获取元素类型,再动态生成数组类(如元素类型为 String 时,数组类为 [Ljava/lang/String;)。

3. arraylength:获取数组长度

用于获取数组的长度,无显式操作数,仅需数组引用。

// ARRAY_LENGTH 获取数组长度
type ARRAY_LENGTH struct {
    base.NoOperandsInstruction
}

func (a *ARRAY_LENGTH) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    // 1. 从栈顶弹出数组引用并校验非空
    arrRef := stack.PopRef()
    if arrRef == nil {
        panic("java.lang.NullPointerException")
    }
    // 2. 获取数组长度并推送回栈顶
    length := arrRef.ArrayLength()
    stack.PushInt(length)
}

// ArrayLength 计算数组长度(根据数组类型返回对应切片长度)
func (o *Object) ArrayLength() int32 {
    switch o.data.(type) {
    case []int8:
        return int32(len(o.data.([]int8)))
    case []uint16:
        return int32(len(o.data.([]uint16)))
    case []int32:
        return int32(len(o.data.([]int32)))
    // 省略其他类型...
    case []*Object:
        return int32(len(o.data.([]*Object)))
    default:
        panic("Not array!")
    }
}

实现逻辑:数组长度本质是底层 Go 切片的长度,通过类型断言获取不同切片的长度并返回。

4. 数组元素访问指令:<t>aload<t>astore

  • <t>aload:从数组指定索引加载元素到操作数栈(如 iaload 加载 int 元素,aaload 加载引用元素);
  • <t>astore:将操作数栈顶元素存入数组指定索引(如 iastore 存储 int 元素,aastore 存储引用元素)。

aaload(引用元素加载)和 iastoreint 元素存储)为例:

// AALOAD 从引用数组加载元素
func (a *AALOAD) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    // 1. 弹出索引和数组引用
    index := stack.PopInt()
    arrRef := stack.PopRef()
    // 2. 校验非空和索引越界
    checkNotNil(arrRef)
    refs := arrRef.Refs() // 获取引用数组([]*Object)
    checkIndex(len(refs), index)
    // 3. 推送元素到栈顶
    stack.PushRef(refs[index])
}

// IASTORE 向 int 数组存储元素
func (i *IASTORE) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    // 1. 弹出值、索引和数组引用
    val := stack.PopInt()
    index := stack.PopInt()
    arrRef := stack.PopRef()
    // 2. 校验非空和索引越界
    checkNotNil(arrRef)
    ints := arrRef.Ints() // 获取 int 数组([]int32)
    checkIndex(len(ints), index)
    // 3. 存储元素
    ints[index] = val
}

// 辅助函数:校验数组非空
func checkNotNil(ref *heap.Object) {
    if ref == nil {
        panic("java.lang.NullPointerException")
    }
}

// 辅助函数:校验索引不越界
func checkIndex(arrLen int, index int32) {
    if index < 0 || index >= int32(arrLen) {
        panic("java.lang.ArrayIndexOutOfBoundsException")
    }
}

通用逻辑:所有元素访问指令均需先校验数组非空和索引合法性,再执行加载或存储操作,区别仅在于元素类型的处理。

四、字符串的实现

Java 字符串通过 java/lang/String 类表示,其核心是字符数组的封装,且通过字符串池实现常量字符串的共享。

1. 字符串的本质:字符数组的封装

String 类的核心字段是 value(字符数组,存储字符串内容)和 hash(缓存哈希值),JVM 中通过对象字段模拟这一结构:

// Java 中的 String 类简化结构
public final class String {
    private final char value[]; // 存储字符串内容
    private int hash; // 缓存哈希值(默认 0)
    // ... 构造器和方法 ...
}

在 JVM 实现中,字符串对象的 data 字段存储字符数组的引用,通过字段访问指令操作 value 数组。

2. 字符串池:常量字符串的共享机制

为节省内存,JVM 对字符串常量采用 “驻留” 机制 —— 相同内容的字符串常量在字符串池中仅存储一份,通过 intern() 方法实现共享。

// 字符串池:key 为 Go 字符串(内容),value 为 Java String 对象
var internedStrings = map[string]*Object{}

// JString 将 Go 字符串转换为 Java String 对象(并驻留到字符串池)
func JString(loader *ClassLoader, goStr string) *Object {
    // 1. 检查字符串池,若已存在则直接返回
    if internedStr, ok := internedStrings[goStr]; ok {
        return internedStr
    }
    // 2. 将 Go 字符串转换为 char 数组([]uint16)
    chars := stringToUtf16(goStr)
    // 3. 创建 char 数组对象("[C" 类)
    jChars := &Object{loader.LoadClass("[C"), chars}
    // 4. 创建 String 对象("java/lang/String" 类)
    jStrClass := loader.LoadClass("java/lang/String")
    jStr := jStrClass.NewObject()
    // 5. 为 String 对象的 "value" 字段赋值(字符数组)
    jStr.SetRefVar("value", "[C", jChars)
    // 6. 存入字符串池
    internedStrings[goStr] = jStr
    return jStr
}

// stringToUtf16 将 Go 字符串转换为 UTF-16 编码的 char 数组([]uint16)
func stringToUtf16(s string) []uint16 {
    runes := []rune(s) // 转换为 Unicode 码点
    chars := make([]uint16, len(runes))
    for i, r := range runes {
        chars[i] = uint16(r)
    }
    return chars
}

核心逻辑

  • 字符串池通过 Go map 实现,键为字符串内容,值为对应的 String 对象;
  • 当创建字符串时,先检查池中有否相同内容的字符串,若有则复用,否则创建新对象并加入池。

五、功能测试

1. 数组测试:冒泡排序验证数组指令

通过冒泡排序算法验证数组的创建、元素访问和修改指令的正确性:

// 测试类:冒泡排序
public class BubbleSortTest {
    public static void main(String[] args) {
        int[] arr = {22, 84, 77, 56, 10, 43, 59};
        int[] ints = bubbleSort(arr);
        for (int anInt : ints) {
            System.out.println(anInt); // 输出排序结果:10 22 43 56 59 77 84
        }
    }

    public static int[] bubbleSort(int[] arr) {
        boolean swapped = true;
        int j = 0;
        int tmp;
        while (swapped) {
            swapped = false;
            j++;
            for (int i = 0; i < arr.length - j; i++) {
                if (arr[i] > arr[i + 1]) {
                    tmp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = tmp;
                    swapped = true;
                }
            }
        }
        return arr;
    }
}

测试结果:排序后的数组元素按从小到大输出,验证 newarrayialoadiastorearraylength 等指令正常工作。

ch08-array-test.png

2. 字符串测试:Hello World 验证字符串池

通过经典的 Hello World 程序验证字符串创建和输出功能:

// 测试类:输出 Hello World
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World"); // 输出字符串
    }
}

测试结果:成功输出 Hello World,验证字符串池、字符数组封装及 println 方法调用的正确性。

ch08-string-test.png

本章小结

本章实现了 JVM 中数组和字符串的核心机制,重点包括:

  1. 数组的特殊实现:数组类由 JVM 动态生成,通过 interface{} 存储不同类型的数组元素,支持基本类型和引用类型数组;
  2. 数组指令集:实现 newarray/anewarray(创建数组)、arraylength(获取长度)、<t>aload/<t>astore(元素访问)等指令,覆盖数组操作全流程;
  3. 字符串机制:通过 java/lang/String 类封装字符数组,利用字符串池实现常量字符串的共享,减少内存占用;
  4. 功能验证:通过冒泡排序和 Hello World 程序验证数组指令和字符串功能的正确性。

数组和字符串的支持是 JVM 功能完整性的重要标志,下一章将讲述本地方法调用与反射的核心机制。

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

评论 (0)

取消