写一个玩具Java虚拟机

Posted 成艮教育

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了写一个玩具Java虚拟机相关的知识,希望对你有一定的参考价值。

本文描述了一个用Java实现的玩具JVM,用Java实现的好处是可以不用处理JVM中的垃圾回收。

Java虚拟机是基于栈的虚拟机。栈虚拟机的特点是所有临时操作数都存放在栈中。编译器生成的指令都会围绕着这个栈展开,相对而言,解释执行这些指令会比较容易。基于栈的虚拟机可能会生成如下指令:

Java .class文件存储的主要就是编译后的指令,一个玩具JVM,简单来说就是解释执行这里面的指令。接下来就说说为了让这个JVM跑起来需要实现哪些功能。

class 文件解析

推荐一下 Java class viewer,里面有个工具可以可视化class文件内容。另外我直接复用了他解析class文件的代码。

class文件描述的信息是以class为单位的,一个类如果有嵌套类,这个嵌套类也会生成为单独的class文件。从c/c++程序员的视角来看,class文件的生成有点类似编译,编译器在编译期间只做依赖符号存在与否的检查。所有引用其他class的地方,不同于c/c++,java class的引用都是在运行期定位的。这里看看一个简单的类class文件结构是怎样的:

写一个玩具Java虚拟机

写一个玩具Java虚拟机

一个class文件比较重要的有:

  • constant pool(常量池):存储字符串字面量、函数原型描述、类成员描述、class引用描述。字节码中经常会引用常量池中的内容,例如要设置某个成员变量,字节码中的操作数就是常量池索引,从索引中获取出具体是哪个成员变量

  • fields:描述类成员变量

  • methods: 描述类成员函数

  • attributes: 分布在很多地方,可能嵌套,用于描述method字节码、调试符号信息等。

常量池非常重要,这里看看class文件中是如何使用常量池的。例如,一个field描述:

写一个玩具Java虚拟机

其中name_indexdescriptor_index的值,指向的就是常量池索引,通过前面推荐的class viewer去常量池中找就会找到对应的值:

写一个玩具Java虚拟机

descriptor_index描述field类型,I指的是整数。Java里有一套描述类型的规则,这个规则在函数定义的地方也会看到。

methods只要有实现,就都会有一个Code attribute,也就是这个函数的具体实现字节码,例如前面的add函数字节码为:

写一个玩具Java虚拟机

解析出class文件中的信息后,玩具JVM就完成一半了。

JVM指令

JVM中已经有200多条指令了。但是这些指令很多都是相似的。具体实现这些指令时可以参考指令表。JVM指令就像x86指令一样,由操作码以及可选的操作数组成。操作码表示具体是哪条指令,占1个字节;操作数表示该操作码需要的参数,变长。class文件中字节码连续存放,像上一节的例子就是4条指令,每条指令只有操作码没有操作数,他们存放在class文件中就是:1B 1C 60 AC。

JVM依次读取这些指令并解释执行。这个过程同真实计算机CPU执行过程类似。用代码描述为:

写一个玩具Java虚拟机

执行环境

线程执行环境

JVM中每个线程都是独立的执行单元。但是对于类符号等信息则是全局共享,堆上创建的对象也是全局可访问的。单个线程中调用函数会产生帧(frame),每一帧都有一个独立的栈用于存储该帧执行的临时数据。从main函数开始执行,每进入一个函数创建一个帧,函数退出(执行return系列指令)清除当前帧。这里的帧也可以被实现为一个栈,当这个栈里没有帧时就表示这个线程退出。这个过程可以描述为:

写一个玩具Java虚拟机

注释中提到了,class文件中method包含了“有多少局部变”、“需要多大的栈”等信息。局部变量的实现是一个数组,数组的下标表示局部变量是第几个。字节码中要访问局部变量时,全部是以这个下标来查找的。例如指令istore_1,表示从栈中弹出一个整数,并写到局部变量1中。

写一个玩具Java虚拟机

全局环境

由于使用Java实现,堆内存的管理完全不用操心。如果我们代码中创建了一个类对象,或者简单点调用了另一个类的静态方法,这个时候会发生什么以及如何处理?例如以下代码:

写一个玩具Java虚拟机

生成以下字节码:

写一个玩具Java虚拟机

可以看出调用静态函数invokestatic的操作数是2,指向的是常量池中的2。工具直接显示了常量池2是一个method,及该method的原型。

遇到这样的指令时,我们就需要找到并加载目标类。所以,全局信息里需要维护类列表。考虑到Java中类与类之间是否相同,除了看类名(全限定名)外,还得看类的加载器(class loader)。所以,玩具JVM中也需要有class loader机制(至少是个简化版)。程序启动时设定一个默认的类加载器,加载主类,执行主类main方法,执行过程中遇到对其他类的引用时,就使用当前类加载器继续加载目标类,如果已经加载就直接返回。

类加载及main方法

前面已经提到了类加载。其实类加载本质上就是把目标class文件加载到内存,保存该class信息。在调用一个类的方法时,也是根据方法名(考虑到方法重载,还得考虑方法的原型,在class file中也就是descriptor)找到具体的方法,根据方法初始化调用帧,以及根据方法获得其要执行的字节码。

所以,我们的JVM要跑起来,也就是找到并加载主类,然后找到主类中的main函数并执行。

写一个玩具Java虚拟机

当然,这个过程严格来说还得判定类访问控制、方法访问控制等。

至此,这个玩具JVM就可以跑起来了。可以设定它的class path,加载类,从main方法开始执行,调用其他类的静态方法,写写阶乘的实现是没有问题了。但是Java中还有很多其他特性:类对象、调用类实例方法、异常处理、调用native方法等待。接下来我再讲讲这些特性的实现,一窥Java核心语法的实现。

扩展实现

类对象及实例方法调用

类对象的创建通过new指令完成,本质上也就是分配个对象,并关联类信息到这个对象。我们的实现中自然会有一个类用来表示玩具JVM中所有的对象:

写一个玩具Java虚拟机

注意这里的Class是我们自己定义的Class,而不是java.lang.Class。Slot类型用于存储整数或一个引用(其他对象)。new指令的大概实现:

写一个玩具Java虚拟机

需要注意的是,当我们在Java中写下代码 new SomeClass() 时,实际会产生两个功能的指令:a) 创建对象;b) 调用类的构造函数( )

写一个玩具Java虚拟机

调用类构造函数同普通类实例方法原理相同,都会先压入对象引用。invokespecial指令用于调用类对象实例方法,从栈顶依次出栈参数,最后出栈类对象实例引用。具体可以看看指令表里的描述。

类静态区域初始化

首次加载某个类时,会执行其static区域代码。这个写测试看下生成的代码就懂了,就是生成一个 的静态方法,在加载类时先执行这个方法。

异常处理

当一个方法中有try/catch时,该方法就会生成出一个异常处理表,存储在Code属性中。如下图:

写一个玩具Java虚拟机

异常处理表每一项都包含:start_pcend_pchandler_pc 及catch_type,表示在start_pc/end_pc间发现异常,且异常类型是catch_type时,则跳转到handler_pc处执行代码,也就是异常处理代码。其中catch_type也是常量池中的索引,当其为0时,则不是常量池索引,而是表示catch所有类型,其实就是finally块。从这里也可以看出,常量池索引是从1开始,而不是0。

当异常发生时,JVM首先从当前帧对应的方法中的异常处理表查找异常处理代码,没有的话则弹出当前帧,回到上一帧,也就是调用者继续查找,直到找完所有调用帧。这个实现相对较多,就不列举代码了。

调用本地方法

前面实现的JVM都没有输出字符串的能力,要提供一个类似System.out.println的方法,就需要注册本地方法到JVM中。这里可以简单地为整个JVM设置一个本地方法表,在JVM启动时完成注册。类似以下代码:

写一个玩具Java虚拟机

也会在class文件中留下一个method,但这个method会被标记为native,自然也没有Code属性,没有字节码可执行。当执行invoke系列指令时,发现调用的是native方法,就需要从全局本地方法表中查找。注册本地方法类似:

本地方法执行时,通过frame参数就可以取出调用该方法传入的参数。

在实现了本地方法后,就可以给这个玩具JVM添加一些系统库,类似OpenJDK中jre目录下的lib。这些系统库可以包含java.lang.System.println、java.lang.String、java.lang.StringBuilder。简单起见,我实现的这些类和Java标准库有些不同。

成艮(北京)教育科技有限公司,致力于建筑、金融科技创新等领域的高端应用型人才培养。业务范围涵盖:ppp项目工程咨询及培训,企业人才定制,着重于高校学生IT、建筑、财会实操培训、就业推荐及创业孵化。

太原校址:山西省太原市晋阳街南二巷三恒煤化工大厦B座九层(省实验中学对面)

赵老师:   18335107166    

靳老师:   13663619020

网    址: www.cybjedu.com

以上是关于写一个玩具Java虚拟机的主要内容,如果未能解决你的问题,请参考以下文章

虚拟机的简介摘录

什么是Java虚拟机?

深入理解Java虚拟机到底是什么?

深入理解Java虚拟机到底是什么(转)

如何写出让java虚拟机发生内存溢出异常OutOfMemoryError的代码

片段(Java) | 机试题+算法思路+考点+代码解析 2023