根据《自己动手写 Java 虚拟机》一书实现的一个简单 JVM,用以学习 JVM & Go。
README 顺序为根据书中目录顺序完成时做的简要记录。
-
类路径加载策略接口:
classpath/entry.go实现根据不同路径参数特征,使用不同策略类加载策略
-
不同类路径加载策略的实现:
- 目录形式的类路经:
classpath/entry_dir.go - 包形式的类路径:
classpath/entry_zip.go - 组合形式的类路径:
classpath/entry_composite.go - 通配符形式的类路径:
classpath/entry_wildcard.go
- 目录形式的类路经:
-
类加载:
classpath/classpath.go根据命令行参数解析启动/扩展类加载器对应路径和用户类加载对应路径;
依次加载启动类加载器,扩展类加载器,用户类加载器对应路径下的类;
具体加载策略根据路径形式的不同,使用
entry的不同实现加载;
Class文件是一组以8位字节为基础单位的二进制流
Class文件格式只包含两种数据类型:
- 无符号数:以u1,u2,u4,u8代表1字节,2字节,4字节,8字节和无符号数。无符号数用来描述数字、索引引用、数量值或按照UTF-8编码构成字符串值
- 表:由多个无符号数或其他表作为数据项构成的符合数据类型,以“_info”结尾
整个Class文件本质也是一个表,其数据项如下:
| 类型 | 名称 | 数量 | 描述 |
|---|---|---|---|
| u4 | magic | 1 | 0xCAFEBABE |
| u2 | minor_version | 1 | 次版本 |
| u2 | major_version | 1 | 主版本 |
| u2 | constant_pool_count | 1 | 常量池容量计数值 |
| cp_info | constant_pool | constant_pool_count - 1 | 常量池 |
| u2 | access_flags | 1 | 访问标志 |
| u2 | this_class | 1 | 类索引 |
| u2 | super_class | 1 | 父类索引 |
| u2 | interfaces_count | 1 | 接口计数值 |
| u2 | interfaces | interfaces_count | 接口索引集合 |
| u2 | fields_count | 1 | 字段表集合计数值 |
| field_info | fields | fields_count | 字段表集合 |
| u2 | methods_count | 1 | 方法表集合计数值 |
| method_info | methods | methods_count | 方法表索引 |
| u2 | attribute_count | 1 | 属性表计数值 |
| attribute_info | attributes | attributes_count | 属性表 |
-
常连池:
- Class文件中的仓库资源,存储自字面量和符号引用
- 每项数据都是一个表,公有14种表类型结构
- 所有类型结构第一位是一个u1类型的标示,用于标示具体的常量类型
- 类型可以大致分为,(详细可以参见常量池的项目类型.png):
- UTF8编码字符串
- 数字字面量
- 字符串字面量
- 类和接口符号引用
- 字段、(类/接口)方法的符号引用
- 字段、方法的部分符号引用
- 动态语言调用支持
- 常量池的项目类型实现:
classfile/cp_*.go
-
字段表
-
用于描述接口或者类中声明的变量,其结构:
类型 名称 数量 描述 u2 access_flags 1 字段访问标志 u2 name_index 1 字段简单名称 u2 descriptor_index 1 字段描述符 u2 attribute_count 1 属性表计数值 attribute_info attributes attribute_count 属性表列表,记录额外信息 -
字段表集合不会从父类继承任何字段
-
Java语言中字段无法重载,字节码中描述符不一致即可重载
-
-
方法表
- 用于描述接口或者类中的方法,其结构与字段表结构完全一致
- 方法内部的实现等其他信息会在属性表中记录
- 如果没有重写父类方法,方法表不会出现来自父类的方法信息
- Java方法签名:方法名,参数类型及顺序;字节码方法签名:方法名,参数类型及顺序,返回值,异常表
-
属性表
-
用于描述某些场景专有信息
-
属性表结构定义较为松散,满足基本定义:
类型 名称 数量 描述 u2 attribute_name_index 1 常量池中CONSTANT_Utf8_info类型常量,表示属性表具体类型 u4 attribute_length 1 属性值占用位数 u1 info attribute_length 属性值,根据不用属性定义 -
部分属性说明:
- Code:记录方法体的代码
- Exceptions:方法中可能抛出的受查异常
- LineNumberTable:源码行号与字节码偏移量之间对应关系
- LocalVariableTable:栈帧中局部变量表中的变量与源码中定义的变量之间的对应关系
- SourceFile:Class文件的源码文件名称
- ConstantValue:通知虚拟机自动为静态变量赋值
- InnerClass:记录内部类与宿主类之间的关联
- Deprecated:表示一个类/字段/方法不再推荐使用
- Synthetic:表示字段或方法不是源码直接产生的
-
私以为上图最简单明了的表示了运行时数据区的结构,大体可以分为线程私有和线程共有。
线程私有部分主要包含两部分:
- PC寄存器:字节码行号指示器
- 虚拟机栈:Java方法执行的内存模型
- 栈帧:与方法对应
- 局部变量表:基本数据类型,对象类型
- 操作数栈:入栈、出栈对应运行期间字节码的写入,提取操作
- 附加信息:动态链接、方法返回地址、其它附件信息
- 栈帧:与方法对应
-
指令集构成:
- 操作码(一个字节,最大可支持256个)
- 操作数(非必须),操作数的来源:字节码、操作数栈
-
JVM架构:面向操作数栈
- 优点:省略填充间隔符号;编译代码精简
- 缺点:操作码总数受限;运行时重建数据结构
-
JVM解释器基本模型:
do { PC值加一; 根据PC值从字节码流取出操作码; if(操作码存在操作数) { 从字节码流中取出操作数 } 执行操作码对应操作 } while(字节码流长度 > 0)
-
字节码
关于字节码更多说明:JVM Opcode Reference
从 JVM 物理结构上划分,类信息与对象信息都集中在 JVM 堆;
从 JVM 逻辑结构上划分,类信息位于方法区(别名 Non-Heap ),对象信息位于 Java 堆;
方法区是运行时数据区中堆的逻辑区域,主要存放从 class 文件获取的类信息、类变量信息。
方法区内的数据:
- 类信息:类的访问标志,类名,超类名,接口名,运行时常量池指针,字段表,方发表,类加载器指针,超类指针,接口指针,实例变量数量,类变量数量,静态变量
- 字段信息:访问标志,名称,描述符,class结构体指针,常量值索引,字段编号
- 方法信息:访问标志,名称,描述符,class结构体指针,操作数栈大小,局部变量表大小,字节码
主要存放两类信息:
- 字面量:整数、浮点数、字符串字面量
- 符号引用:类符号引用,字段符号引用、方法符号引用、接口符号引用
对于加载类,其加载过程大致可以分为三大步:加载、连接、初始化;
其中初始化过程又可细分为验证、准备、解析;
完整的过程:
- 加载
- 将 class 文件读入内存
- 解析二进制数据流,生成类数据保存在方法区
- 根据 Class 对象,作为类的类数据的访问入口
- 验证:确保 Class 文件字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全
- 准备:类、实例变量的编号,类变量赋初始值
- 解析:将常量池内的符号引用替换为直接引用
- 初始化:执行类初始化方法(())
方法区内类信息来自 class 文件的内容,但会做响应解析,赋值。
例如类的成员(字段、方法)会生成对应的运行时数据结构,常量的符号引用也会解析成运行时的数据结构。
-
方法分类:
调用角度方法的分类:静态方法、实例方法
实现角度方法的分类:抽象方法、普通方法、本地方法
JVM 方法调用的指令分类:
-
invokestatic:调用静态方法
-
invokespecial:无需动态绑定的实例方法(构造函数、私有方法、父类方法)
-
invokeinterface:接口方法调用
-
invokevirtual:虚方法调用
-
invoked dynamic:不讨论
(区分接口方法和普通方法的原因:普通方法可以使用虚方法表(Virtual Method Table)技术优化)
-
-
方法调用的一般过程:
- 根据方法调用指令操作码的操作数(方法符号索引)解析得到方法直接引用
- 确定最终调用方法
- 为方法创建一个新的栈帧,并推送到栈顶
- 传递参数
- 解释器执行方法内容
- 返回值推入前一帧的操作数栈顶
-
Java 方法调用各阶段的工作:
-
编译阶段:静态分派(重载),根据方法接受者,方法参数静态类型生成指令
-
类加载阶段:解析调用,将符号引用转变为可确定的直接引用
-
运行阶段:动态分派(重写),依据指令根据方法的接受者的实际类型寻找方法直接引用
总结:Java 是一门静态多分派(方法接受者,方法参数),动态单分派(方法接收者)的语言
-
-
类初始化与方法调用
- 类初始化阶段是执行类构造器
<client>()方法的过程 - 关于
<client>()方法的说明:- 由编译器自动收集类变量的复制动作和静态语句块的语句合并产生,类和接口都可以有
<client>() - 不需要显式调用父类
<client>()JVM 在执行子类<client>()前会确保父类的先执行,但不适用于接口 <client>()不是必须的- JVM 保证
<client>()在多线程环境下线程安全 - 对同一个类加载器,一个类型只初始化一次
- 由编译器自动收集类变量的复制动作和静态语句块的语句合并产生,类和接口都可以有
- 类初始化阶段是执行类构造器
数组基本分类:
- 基本类型数组
- 引用类型数组
数组类的特殊点:
- 数组类是由 JVM 运行时生成并且不需要进行类初始化动作
- 数组类的超类都是 Object,实现 Cloneable 和 Serializable 接口
- 数组可以强制转换成超类类型,接口类型
- 数组之间的强制类型转换(类型为 []SC 的数组可以强制转换成类型为 []TC 的数组 )
- TC 和 SC 是同一个基本类型
- TC 和 SC 都是引用类型,且 SC 可以强制转换成 TC
- 数组类名:[数组元素类型描述符
- 数组类型描述符就是类名
JVM 创建数组的指令:
- newarray:创建基本类型数组
- anewarray:创建引用类型数组
- multianewarray:创建多维数组
class 文件中字符串保存格式:MUTF8
运行期字符串保存格式:UTF16
JVM 有专门的字符串池,用于存储字符串常量
OpenJDK 类库中的本地方法使用 JNI 编写
GJvm 使用 Go 语言实现部分本地方法,JVM 对本地方法的调用转换为对 Go 实现方法的调用
- 注册:使用字典结构保存方法唯一标识、Go 函数
- 调用:使用 JVM 预留的指令 0xFE,作为本地方法的调用指令
对本地方法的调用即:
- 从注册的本地方法中查找对应的 Go 函数,调用该函数
- 根据本地方法返回值类型执行对应 RETURN 语句
GJvm中,类与类对象创建先后:
- 创建类加载器时,加载 java.lang.Class
- java.lang.Class 触发父类 java.lang.Object 加载,以上过程均布创建对象
- java.lang.Class 加载完成为方法区内的类创建类对象
- 之后所有的类加载时创建对应的类实例
-
void 和基本类型的类名就是本身名称
-
基本类型的类没有超类
-
非基本类型的类对象通过 ldc 指令加载到操作数栈
基本类型的类通过 getstatic 指令访问相应包装类的 TYPE 字段加载到操作数栈
- catch:捕获特定异常,然后处理返回
- throw:抛出异常给调用者,直到有方法处理或到达 JVM 栈底
异常捕获,处理逻辑的实现:方法的异常处理表
| 类型 | 名称 | 含义 |
|---|---|---|
| u2 | start_pc | 异常捕获起始行(方法体开始偏移量) |
| u2 | end_pc | 异常捕获结束行(不包含,方法开始偏移量) |
| u2 | handler | 异常处理位置(方法体开始偏移量) |
| u2 | catch_type | 异常类型,常量类型索引 |
- 获取:创建异常实例创建时调用 Throwable 的fillInStackTrace 本地方法获取
- 记录:创建异常对象时在本地方法中获取虚拟机栈信息,记录在 extra 字段中
-
关于栈
整个实现的过程中存在多个地方提及栈:虚拟机栈,栈帧,操作数栈,三者实际是不同级别的东西,但看着看着还是容易混。
- 虚拟机栈(Stack):数据结构上的栈,其元素为栈帧
- 栈帧(Frame):数据结构上的栈元素,Java方法内存模型,存储操作数栈(以及其它)
- 操作数栈(OperandStack):数据结构上的栈,存储指令操作的上下文环境信息
一个线程对应唯一一个虚拟机栈,一个虚拟机栈中有若干个栈帧,每个栈帧又包含一个操作数栈,一个操作数栈则有若干个操作数。
-
PC
PC 表示程序执行的字节码行号,实现过程中有两个 PC 且定义层级不同:
- Thread.pc
- Frame.nextPC
二者含义都与 PC 有关,在 Frame 中定义 nextPC 主要是为了指令跳转使用
func Branch(frame *rtda.Frame, offset int) { pc := frame.Thread().PC() nextPC := pc + offset frame.SetNextPC(nextPC) }
其它参考:
命令行解析:Go by Example 中文:命令行标志
类加载:《深入理解 Java 虚拟机》7.4
类解析:《深入理解 Java 虚拟机》6.1, 6.2, 6.3
运行时数据区(线程私有数据):《深入理解 Java 虚拟机》2.2, 8.2
字节码及解析:《深入理解 Java 虚拟机》6.4, 8.4,JVM Opcode Reference
描述符含义的说明:JVM指令:对象类型和方法签名
类加载过程:《深入理解 Java 虚拟机》7.1, 7.2, 7.3
符号引用的解析:《深入理解 Java 虚拟机》7.3.4
方法调用:《深入理解 Java 虚拟机》6.4.8, 8.3
类初始化:《深入理解 Java 虚拟机》7.3.5
异常处理相关:《深入理解 Java 虚拟机》6.3.7, 6.4.9