前言
在前一章中,我们实现了方法调用与返回机制,支撑了函数执行的核心流程。本章将聚焦 JVM 中数组和字符串的实现—— 这两类数据结构在 Java 中使用频繁,但它们的创建、存储和操作逻辑与普通对象存在显著差异。数组类由 JVM 运行时动态生成,而非从 Class 文件加载;字符串则通过常量池和字符串池实现共享。本章将详细实现这些特性,完善 JVM 对复杂数据结构的支持。
参考资料
《自己动手写 Java 虚拟机》—— 张秀宏
开发环境
| 工具 / 环境 | 版本 | 说明 |
|---|---|---|
| 操作系统 | MacOS 15.5 | 基于 Intel/Apple Silicon 均可 |
| JDK | 1.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,实现 Cloneable 和 Serializable 接口 |
核心差异:数组类的结构由 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,实现 Cloneable 和 Serializable),无需像普通类那样从 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!")
}
}执行流程:
- 从操作数栈获取数组长度并校验非负;
- 根据
atype确定数组类型(如AT_INT对应[I类); - 创建数组对象并将引用推送回操作数栈。
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(引用元素加载)和 iastore(int 元素存储)为例:
// 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;
}
}测试结果:排序后的数组元素按从小到大输出,验证 newarray、iaload、iastore、arraylength 等指令正常工作。

2. 字符串测试:Hello World 验证字符串池
通过经典的 Hello World 程序验证字符串创建和输出功能:
// 测试类:输出 Hello World
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World"); // 输出字符串
}
}测试结果:成功输出 Hello World,验证字符串池、字符数组封装及 println 方法调用的正确性。

本章小结
本章实现了 JVM 中数组和字符串的核心机制,重点包括:
- 数组的特殊实现:数组类由 JVM 动态生成,通过
interface{}存储不同类型的数组元素,支持基本类型和引用类型数组; - 数组指令集:实现
newarray/anewarray(创建数组)、arraylength(获取长度)、<t>aload/<t>astore(元素访问)等指令,覆盖数组操作全流程; - 字符串机制:通过
java/lang/String类封装字符数组,利用字符串池实现常量字符串的共享,减少内存占用; - 功能验证:通过冒泡排序和 Hello World 程序验证数组指令和字符串功能的正确性。
数组和字符串的支持是 JVM 功能完整性的重要标志,下一章将讲述本地方法调用与反射的核心机制。
源码地址:https://github.com/Jucunqi/jvmgo.git
评论 (0)