Thanks to visit codestin.com
Credit goes to github.com

Skip to content

zhanghTK/go-jvm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GJvm

根据《自己动手写 Java 虚拟机》一书实现的一个简单 JVM,用以学习 JVM & Go。

README 顺序为根据书中目录顺序完成时做的简要记录。

实现过程

类加载

  • 类路径加载策略接口:classpath/entry.go

    实现根据不同路径参数特征,使用不同策略类加载策略

  • 不同类路径加载策略的实现:

    1. 目录形式的类路经:classpath/entry_dir.go
    2. 包形式的类路径:classpath/entry_zip.go
    3. 组合形式的类路径:classpath/entry_composite.go
    4. 通配符形式的类路径: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:表示字段或方法不是源码直接产生的

运行时数据区(线程私有数据)

运行时数据区结构.png

私以为上图最简单明了的表示了运行时数据区的结构,大体可以分为线程私有和线程共有。

线程私有部分主要包含两部分:

  • PC寄存器:字节码行号指示器
  • 虚拟机栈:Java方法执行的内存模型
    • 栈帧:与方法对应
      • 局部变量表:基本数据类型,对象类型
      • 操作数栈:入栈、出栈对应运行期间字节码的写入,提取操作
      • 附加信息:动态链接、方法返回地址、其它附件信息

字节码(指令)及解析

  • 指令集构成:

    • 操作码(一个字节,最大可支持256个)
    • 操作数(非必须),操作数的来源:字节码、操作数栈
  • JVM架构:面向操作数栈

    • 优点:省略填充间隔符号;编译代码精简
    • 缺点:操作码总数受限;运行时重建数据结构
  • JVM解释器基本模型:

    do {
      PC值加一;
      根据PC值从字节码流取出操作码;
      if(操作码存在操作数) {
        从字节码流中取出操作数
      }
      执行操作码对应操作
    } while(字节码流长度 > 0)
  • 字节码

    关于字节码更多说明:JVM Opcode Reference

类与对象(运行时数据区中线程公有数据)

从 JVM 物理结构上划分,类信息与对象信息都集中在 JVM 堆;

从 JVM 逻辑结构上划分,类信息位于方法区(别名 Non-Heap ),对象信息位于 Java 堆;

方法区

方法区是运行时数据区中堆的逻辑区域,主要存放从 class 文件获取的类信息、类变量信息。

方法区内的数据:

  • 类信息:类的访问标志,类名,超类名,接口名,运行时常量池指针,字段表,方发表,类加载器指针,超类指针,接口指针,实例变量数量,类变量数量,静态变量
  • 字段信息:访问标志,名称,描述符,class结构体指针,常量值索引,字段编号
  • 方法信息:访问标志,名称,描述符,class结构体指针,操作数栈大小,局部变量表大小,字节码

运行时常量池

主要存放两类信息:

  • 字面量:整数、浮点数、字符串字面量
  • 符号引用:类符号引用,字段符号引用、方法符号引用、接口符号引用

类加载器

对于加载类,其加载过程大致可以分为三大步:加载、连接、初始化;

其中初始化过程又可细分为验证、准备、解析;

完整的过程:

  1. 加载
    1. 将 class 文件读入内存
    2. 解析二进制数据流,生成类数据保存在方法区
    3. 根据 Class 对象,作为类的类数据的访问入口
  2. 验证:确保 Class 文件字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全
  3. 准备:类、实例变量的编号,类变量赋初始值
  4. 解析:将常量池内的符号引用替换为直接引用
  5. 初始化:执行类初始化方法(())

方法区内类信息来自 class 文件的内容,但会做响应解析,赋值。

例如类的成员(字段、方法)会生成对应的运行时数据结构,常量的符号引用也会解析成运行时的数据结构。

方法调用和返回

  • 方法分类:

    调用角度方法的分类:静态方法、实例方法

    实现角度方法的分类:抽象方法、普通方法、本地方法

    JVM 方法调用的指令分类:

    • invokestatic:调用静态方法

    • invokespecial:无需动态绑定的实例方法(构造函数、私有方法、父类方法)

    • invokeinterface:接口方法调用

    • invokevirtual:虚方法调用

    • invoked dynamic:不讨论

      (区分接口方法和普通方法的原因:普通方法可以使用虚方法表(Virtual Method Table)技术优化)

  • 方法调用的一般过程:

    1. 根据方法调用指令操作码的操作数(方法符号索引)解析得到方法直接引用
    2. 确定最终调用方法
    3. 为方法创建一个新的栈帧,并推送到栈顶
    4. 传递参数
    5. 解释器执行方法内容
    6. 返回值推入前一帧的操作数栈顶
  • Java 方法调用各阶段的工作:

    • 编译阶段:静态分派(重载),根据方法接受者,方法参数静态类型生成指令

    • 类加载阶段:解析调用,将符号引用转变为可确定的直接引用

    • 运行阶段:动态分派(重写),依据指令根据方法的接受者的实际类型寻找方法直接引用

      总结:Java 是一门静态多分派(方法接受者,方法参数),动态单分派(方法接收者)的语言

  • 类初始化与方法调用

    • 类初始化阶段是执行类构造器 <client>()方法的过程
    • 关于<client>()方法的说明:
      • 由编译器自动收集类变量的复制动作和静态语句块的语句合并产生,类和接口都可以有<client>()
      • 不需要显式调用父类<client>()JVM 在执行子类<client>()前会确保父类的先执行,但不适用于接口
      • <client>()不是必须的
      • JVM 保证<client>()在多线程环境下线程安全
      • 对同一个类加载器,一个类型只初始化一次

数组&字符串

数组

数组基本分类:

  • 基本类型数组
  • 引用类型数组

数组类的特殊点:

  1. 数组类是由 JVM 运行时生成并且不需要进行类初始化动作
  2. 数组类的超类都是 Object,实现 Cloneable 和 Serializable 接口
  3. 数组可以强制转换成超类类型,接口类型
  4. 数组之间的强制类型转换(类型为 []SC 的数组可以强制转换成类型为 []TC 的数组 )
    • TC 和 SC 是同一个基本类型
    • TC 和 SC 都是引用类型,且 SC 可以强制转换成 TC
  5. 数组类名:[数组元素类型描述符
  6. 数组类型描述符就是类名

JVM 创建数组的指令:

  • newarray:创建基本类型数组
  • anewarray:创建引用类型数组
  • multianewarray:创建多维数组

字符串

class 文件中字符串保存格式:MUTF8

运行期字符串保存格式:UTF16

JVM 有专门的字符串池,用于存储字符串常量

本地方法

注册于调用

OpenJDK 类库中的本地方法使用 JNI 编写

GJvm 使用 Go 语言实现部分本地方法,JVM 对本地方法的调用转换为对 Go 实现方法的调用

  • 注册:使用字典结构保存方法唯一标识、Go 函数
  • 调用:使用 JVM 预留的指令 0xFE,作为本地方法的调用指令

对本地方法的调用即:

  1. 从注册的本地方法中查找对应的 Go 函数,调用该函数
  2. 根据本地方法返回值类型执行对应 RETURN 语句

本地方法的实现

反射

GJvm中,类与类对象创建先后:

  1. 创建类加载器时,加载 java.lang.Class
  2. java.lang.Class 触发父类 java.lang.Object 加载,以上过程均布创建对象
  3. java.lang.Class 加载完成为方法区内的类创建类对象
  4. 之后所有的类加载时创建对应的类实例
基本类型的类
  1. void 和基本类型的类名就是本身名称

  2. 基本类型的类没有超类

  3. 非基本类型的类对象通过 ldc 指令加载到操作数栈

    基本类型的类通过 getstatic 指令访问相应包装类的 TYPE 字段加载到操作数栈

异常

异常处理方式

  1. catch:捕获特定异常,然后处理返回
  2. throw:抛出异常给调用者,直到有方法处理或到达 JVM 栈底

异常捕获,处理逻辑的实现:方法的异常处理表

类型 名称 含义
u2 start_pc 异常捕获起始行(方法体开始偏移量)
u2 end_pc 异常捕获结束行(不包含,方法开始偏移量)
u2 handler 异常处理位置(方法体开始偏移量)
u2 catch_type 异常类型,常量类型索引

堆栈信息

  • 获取:创建异常实例创建时调用 Throwable 的fillInStackTrace 本地方法获取
  • 记录:创建异常对象时在本地方法中获取虚拟机栈信息,记录在 extra 字段中

记录

  1. 关于栈

    整个实现的过程中存在多个地方提及栈:虚拟机栈,栈帧,操作数栈,三者实际是不同级别的东西,但看着看着还是容易混。

    • 虚拟机栈(Stack):数据结构上的栈,其元素为栈帧
    • 栈帧(Frame):数据结构上的栈元素,Java方法内存模型,存储操作数栈(以及其它)
    • 操作数栈(OperandStack):数据结构上的栈,存储指令操作的上下文环境信息

    一个线程对应唯一一个虚拟机栈,一个虚拟机栈中有若干个栈帧,每个栈帧又包含一个操作数栈,一个操作数栈则有若干个操作数。

  2. 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

About

JVM implemented by Golang

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages