# 简单的jvm编译器 **Repository Path**: ni-zewen/simple-jvm-compiler ## Basic Information - **Project Name**: 简单的jvm编译器 - **Description**: 用GO实现简单的jvm编译器 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2021-09-07 - **Last Updated**: 2023-01-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: Go语言 ## README > 此文件为 jvm 编译器和及其知识的相关说明 > 包括 > - 知识补充 > - 运行时的数据区域 > - 编译器解析 > - 命令行工具 > - 搜索 class > - 解析 class 文件 > - 运行时数据区 > - 指令集和解释器 > - 类与对象 > 引用文章 > https://blog.csdn.net/ylyuanlu/article/details/18947951 栈帧详解 > https://github.com/Snailclimb/JavaGuide jvm 部分 > https://www.cnblogs.com/pingxin/p/p00081.html 用 go 实现 jvm > 《深入浅出 Java 虚拟机》 > 《Java 高并发之美》 ## 基础知识 - **栈帧** 表示程序的函数调用记录,而栈帧又是记录在栈上面,很明显栈上保持了 N 个栈帧的实体,(实际上我们这里说的栈帧是软件上的概念,据说有硬件概念,不是很了解),那就可以说栈帧将栈分割成了 N 个记录块,但是这些记录块大小不是固定的,因为栈帧不仅保存诸如:函数入参、出参、返回地址和上一个栈帧的栈底指针等信息,还保存了函数内部的自动变量(甚至可以是动态分配内存,alloca 函数就可以实现,但在某些系统中不行),因此,不是所有的栈帧的大小都相同。 - **jVM 是虚拟机**,总的来说是一种标准规范,虚拟机有很多实现版本。主要作用就是运行 java 的类文件的。 **HotSpot 是虚拟机的一种实现**,它是 sun 公司开发的,是 sun jdk 和 open jdk 中自带的虚拟机,同时也是目前使用范围最广的虚拟机。 - **句柄**,用一种形象的说法可以表述为:有一个固定的地址(句柄),指向一个固定的位置(区域 A),而区域 A 中的值可以动态地变化,它时刻记录着当前时刻对象在内存中的地址。这样,无论对象的位置在内存中如何变化,只要我们掌握了句柄的值,就可以找到区域 A,进而找到该对象。而句柄的值在程序本次运行期间是绝对不变的,我们(即系统)当然可以掌握它。这就是以不变应万变,按图索骥,顺藤摸瓜。(相当于**指针的指针**) ## Ⅰ 知识补充 ### 1. 概述 虚拟机自动内存管理的机制下,不再需要像 C/C++程序开发那样为 new 操作写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出。所以一旦出现也难于排查,需要了解虚拟机的怎么样使用内存。 ### 2. 运行时的数据区域 ![avator](images/Memory.png) JDK1.7 中已经将运行时常量池从方法区中移除,在 Java 堆中开辟一块区域存放常量池。 #### 2.1 程序计数器 (指示器) 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器**。**字节码解释器工作时通过改变这个计数器的值来选取一条需要执行的字节码指令,分支循环异常处理线程恢复等功能都需要依赖计数器完成** 所以为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,称这类内存区域为“**线程私有**”的内存 其主要有俩个作用 - 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制 - 在多线程的情况下,程序计数器用来记录当前线程执行的位置,用来恢复现场 #### 2.2 Java 虚拟机栈 (索引) Java 虚拟机栈也是现场私有的,其生命周期和线程相同,**描述 Java 方法执行的内存模型**。 Java 内存可以粗糙的分为堆内存和栈内存,其中栈就是现在说的虚拟机栈,即**虚拟机中局部变量表的部分**(实际上,Java 虚拟机栈是由一个个栈帧组成,每个栈帧中都有局部变量表,操作数栈,动态链接,方法出口信息。) **局部变量表主要存放编译器可知的各种数据类型,对象引用**(reference 类型,可能是一个指向对象起始地址的**引用指针**,也可能是指向**一个代表对象的句柄或其他与此对象相关的位置**)

Java 虚拟机栈会出现俩种异常:StackOverFlowError 和 OutOfMemoryError

- **StackOverFlowError** Java 虚拟机栈的内存不允许动态拓展,当请求栈的深度超过 - 当前 Java 虚拟机栈的最大深度,就抛出 StackOverFlowError 异常 **OutOfMemoryError** Java 虚拟机栈的内存允许动态拓展,且当线程请求时栈内存用完 **Java 虚拟机栈也是线程私有的,每个线程有各自的虚拟机栈**,且生命周期和线程相同 #### 2.3 本地方法栈 **虚拟机栈为虚拟机执行的 Java 方法(字节码)服务,而本地方法栈使用到的 native 方法服务** 在 HotSpot 和 Java 虚拟机中合二为一。 其调用方式,储存方式,异常抛出方式和虚拟机栈类似。 #### 2.4 堆 (内存) Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此区域的唯一目的是存放对象实例,几乎所有的对象实例和数组都在这里分配内存。** **Java 堆是垃圾收集器管理的主要区域,所以也被称作 GC 堆(Garbage Collected Heap)** 从垃圾回收的角度 Java 堆可分为 Eden 空间等。**进一步划分的目的是为了更好的回收内存,或更快的分配内存** 在 JDK1.8 中移除整个永久代,取而代之的一个元空间(前者使用 JVM 堆,后者使用的是物理内存) #### 2.5 方法区 (常量和类信息) **方法区和 Java 堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据**虽然是堆的逻辑部分,但其别名是非堆。 **相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就永久存在了** #### 2.6 运行时的常量池 运行时常量池是方法区的一部分。Class 文件中除了类的版本,字段,方法等描述信息外,还有常量池信息

当常量池无法再申请到内存时会抛出OutOfMemoryRerror异常

#### 2.7 直接内存 其不是虚拟机运行时的一部分,但是也被频繁的使用,

且也有可能导致OutOfMemoryError的出现

JDK1.4 中新加入的 NIO 类引入基于通道的缓冲区 I/O 方式,可以直接使用 Native 函数库直接分配堆外内存,然后通过 Java 堆中的对象作为此内存的引用直接操作,避免了 Java 堆和 Native 堆之间来回复制数据。(**此时堆中的对象作为引用,之间内存中的区域作为储存空间**) ### 3. HotSpot 虚拟机对象解密 HotSpot 虚拟机在 Java 堆中对象分配,布局和访问的全过程

下列内容需要熟练掌握

#### 3.1 对象的创建 ![avator](images/create.png) 1. **类加载检查** 虚拟机遇到一条 **new 指令**时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过,解析和初始化过。如果没有,则需要先执行相应的类的加载过程。 2. **分配内存** 在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需要的内存大小在类加载完成后便可以确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分除了。**分配方式有指针碰撞和空闲列表俩种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。** ![avator](images/MemoryContri.png)

下列内容尚未理解

> **内存分配的并发问题** > 在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发中,创建对象是很频繁的事情。作为虚拟机来说,必须要保证线程是安全的,虚拟机采用俩种方式来保证线程的安全。 > > - CAS+失败重试 CAS 是乐观锁的一种实现方式,**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。** > - TLAB 3. **初始化零值** 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步保证了对象的实例字段在 Java 代码中可以不赋初值就直接使用,程序能访问到这些字段对应的零值。 4. **设置对象头** 初始化零值后,**虚拟机要对对象进行必要的设置**,例如这个对象室那个类的实例,如何才能找到类的元数据信息,对象的哈希码等,这些信息存在对象头中。 5. **执行 init 方法** 在上面的工作都完成后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的角度来看,对象创建才刚开始。一般来说,**执行 new 指令之后会接着执行\方法,把对象按照程序员的意愿进行初始化,这样一个真正可以用的对象才算真正的产生出来** #### 3.2 对象的内存布局 对象在内存中的布局可以分为三块区域:**对象头,实例数据和对齐填充** Hotspot 虚拟机的**对象头**包括俩部分信息,第一部分**用于储存对象自身的自身运行时数据**,另一部分是**类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定**这个对象是那个类的实例**(相当于索引和自身配置数据) **实例数据**部分是对象真正储存的有效信息,也是程序中所定义的各种类型的各种类型的字段内容。 **对齐填充部分**不是必然存在的,仅仅起到占位作用,因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。 #### 3.3 对象的访问定位 建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由俩种,**一是使用句柄,二是使用直接指针** - 使用句柄的化,Java 队中划分出一块内存来作为句柄池,reference 中存的就是对象的句柄地址 ![avator](images/句柄.png) - 直接指针,那么 Java 堆中对象的就必须考虑如何放置类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 ![avator](images/直接指针.png) 这俩种对象访问方式各有优势,使用句柄来访问的最大好处是 reference 种存储的是稳定的句柄地址,在对象被移动时只会改变句柄种的实例数据指针,而 reference 本身不需要修改。使用直接指针最好的好处就是速度快,节省了一次指针定位的时间开销。 ## Ⅱ 编译器解析 ### 使用方式 - -Xjre ### 一 命令行工具 #### 1.什么是命令行工具 Java 是通过 java 虚拟机来装载和执行编译文件(class 文件)的,java 虚拟机通过命令`java option`来启动,`-option`为虚拟机参数,通过这些参数可对虚拟机的运行状态进行调整. #### 2.package main 解析(详情见注释) - 调用 parseCmd()函数解析输入的命令行命令行参数 - 在 parseCmd()函数中将命令行参数赋值给全局 cmd 结构体 - 在 main 函数中对 cmd 结构体中的各项属性进行解析并判断 ### 二 搜索 class #### 1.什么是类路径 - Java 类路劲告诉 java 解释器和 javac 编译器去哪里找到要执行和导入的类,类路径由启动类路径,扩展类路径和用户类路径构成 - 设置 Java 类路径,在运行时进行,每次启动 Java 应用程序和 JVM,都要指定类路径。运行时使用 -cp 选项来指定类路径,这里的运行时是指启动应用程序和 JVM 时。 例如 C :/Cloudscape_10.0/demo/programs/simple>java -cp %CLOUDSCAPE_INSTALL%/lib/cs.jar; SimpleApp - 启动类路径在 rt.jar 下 ![avator](../jvmgo/images/rtJar.png) #### 2.package classpath 解析(详情见注释) - 套用组合模式实现 `classpath` - 一种将对象组合成树状的层次结构模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象有一致的访问性,属于结构型设计模式 - "http://c.biancheng.net/view/1373.html" - Entry 接口的具体设计 - 有俩个方法,`readClass()`负责寻找和加载类路径,string()用于返回变量的字符串表示 - Entry 接口有四个实现,分别是 `DirEntry,ZipEntry,CompositeEntry,WildcarEntry` - 每个实现文件有一个 struct 结构体储存路径,有一个构造函数(?)拼接类路径,还有 readClass()和 string()的具体实现 - 搜索过程 - 传入类名,调用 Abs 方法获取绝对路径。 - 根据绝对路径打开后遍历文件获取需要的类。 - ### 三 解析 class 文件 #### 1.理解 JAVA Class 文件 所有 Java 文件必须被编译成 class 文件之后才会被 jvm 所识别和运用。Java 虚拟机中定义的 Class 文件格式,每一个 Class 文件都对应唯一一个类或接口的定义信息,但是类或接口不一定都在文件中(**如类和接口可以通过类加载器直接生成**) 每个 Class 文件都是由 8 字节为单位的字节流组成,多字节数据项总是按照 Big-Endian 的顺序进行储存。 Class 文件使用类似 C 语言的伪结构来描述 Class 文件格式,用于描述类结构的内容定义为项(Item),表(Table)是由任意数量的可变长度的项组成 表(Table)是由任意数量的可变长度的项组成,用于表示 Class 文件内容的一系列复合结构。尽管我们采用类似 C 语言的数组语法来表示表中的项,但是我们应当清楚意识到,表是由可变长数据组成的复合结构(表中每项的长度不固定),因此无法直接将字节偏移量来作为索引对表进行访问。而我们描述一个数据结构为数组(Array)时,就意味着它含有零至多个长度固定的项组成,这个时候则可以采用数组索引的方式来访问它。 ```C++ // ClassFile的基本机构 ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; //常量池,constant_pool是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。 cp_info constant_pool[constant_pool_count-1]; //常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。 u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } ``` #### 2.classfile 包各文件解析(详情见注释) - class_reader.go: 读取数据 ### 四 运行时的数据区 这一阶段主要由五个阶段,分别是加载,验证,准备,解析,初始化 #### 1.理解数据区 ![avator](https://img-blog.csdn.net/20171014180538873?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMTQ2NDUzNg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) - 方法区 方法区是各个线程共享的内存区域,用于储存被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码,运行时常量池。在 HotSpot 上也被称为"永久代" #### 2.加载 ##### 1.加载过程 - 通过一个类的全限定名来获取定义此类的二进制字节流 - 将这个字节流所代表的静态数据结构转换为方法区的运行时的数据结构(**即三中的 classFile 类**) - 在内存中生成一个代表这个类的 java.lang.Class 对象,作为类的访问入口 ### 五 指令集和解析器 ### 六 类与对象 > 问题记录 > > 1. jdk 版本更迭下 根目录,项目目录,classpath 目录 的路径 > 本项目使用的 jdk 为 jdk7,`G:\JavaAll\jdk7\jre\lib\rt.jar` 中包含根路径,但是原先的 jdk11 不行,没有 rt.jar,不同版本的 jdk 中目录存在差异