要去“正确地”实现一台Java虚拟机,其实并不像大多数人所想的那样高深和困难——只需要正确读取class文件中每一条字节码指令,并且能正确执行这些指令所蕴含的操作即可。
class文件格式
编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件和操作系统)的二进制格式表示,并且经常以文件的形式存储,因此这种格式称为class文件格式。
数据类型
与Java程序语言的数据类型相似,Java虚拟机可以操作的数据类型可分为两类:原始类型(primitive type)和引用类型(reference type)。
其中,原始类型包括数值类型、boolean类型和returnAddress类型三类。数值类型又分为整数类型(byte、short、int、long、char)和浮点类型(float、double)。boolean类型经过编译之后使用Java虚拟机中的int数据类型代替。returnAddress类型的值指向一条虚拟机指令的操作码。
引用类型包括类类型、数组类型和接口类型。引用类型的值中还有一个特殊的值:null,表示当前引用不指向任何对象。
运行时数据区
Java虚拟机定义了若干程序运行期间都使用的运行时数据区,其中一些会随着虚拟机启动而创建,随着虚拟机销毁而销毁。另外一些则是与线程是相对应的,这些与线程相对应的数据区域随着线程的开始和结束而创建和销毁。
pc寄存器
Java虚拟机可以支持多条线程同时执行,每一条线程都有自己的pc寄存器。在任意时刻,一条Java虚拟机线程都会执行一个方法的代码,此方法称为当前方法。如果当前方法是非native的,那pc寄存器就保存Java虚拟机当前执行的字节码指令的地址,如果该方法是native的,那pc寄存器的值就是undefined。
Java虚拟机栈
每一条Java虚拟机线程都有自己私有的Java虚拟机栈,Java虚拟机栈是以栈帧为基本单位的,主要用于存储局部变量与一些尚未算好的结果。它在方法调用和返回中扮演了很重要的角色,除了栈帧(一个栈帧对应一个方法,即每次进入一个新的方法时都会创建一个新栈帧)的出栈和入栈之外,Java虚拟机栈不再受其他因素的影响。
Java堆
在Java虚拟机中,堆是可供各个线程共享的运行时内存区域,也是供所有类的实例和数组对象分配内存的区域。我们常说的垃圾自动回收就主要针对堆中的垃圾而言的。堆中的对象被garbage collector(垃圾收集器)所管理。
方法区
在Java虚拟机中,方法区是可供各个线程共享的运行时内存区域,主要用于存储每一个类的结构信息。例如:运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
运行时常量池
运行时常量池是class文件中每一个类或接口的常量池表的运行时表现形式,它包括了若干种不同的常量,从编译期可知的数值字面量到必须在运行期解析后才能获得的方法或字段的引用。运行时常量池都在Java虚拟机的方法区分配。
本地方法栈
Java虚拟机实现可能会使用到传统的栈来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈。如果Java虚拟机不支持native方法,则不提供本地方法栈。
栈帧
栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态链接、方法返回值和异常分派。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成都算方法结束。
栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表、操作数栈和指向当前方法所属类的运行时常量池的引用。
局部变量表
Java虚拟机使用局部变量表来完成方法调用时的参数传递。当调用类方法时,它的参数将会依次传入到局部变量表从0开始的连续位置上。当调用实例方法时,第0个局部变量一定用来存储该实例方法所在对象的引用(this)。后续的变量依次存储从1开始的连续位置上。
操作数栈
栈帧在刚刚创建时,操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把操作结果重新入栈。在调用方法时,操作数栈也用来准备调用方法的参数以及接受方法返回结果。
运行时常量池的引用
通过这个引用,以便对当前方法的代码实现动态链接。在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些符号引用所表示的方法转换为对实际方法的直接引用。
字节码指令简介
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码以及跟随其后的零至多个代表此操作所需参数的操作数所构成。下面介绍下一些常见的指令。
加载和存储指令
加载和存储指令用于将数据从栈帧的本地变量表和操作数栈之间来回传递。
常见的指令:
- 将一个本地变量加载到操作数栈:iload、lload、dload等。
- 将一个数值从操作数栈存储到局部变量表:istore、fstore、astore等。
- 将一个常量加载到操作数栈:bipush、sipush、ldc等。
算数指令
算数指令用于对两个操作数栈上的值进行某种特定运算,并把结构重新压入操作数栈。
常见的指令:
- 加法指令:iadd、ladd、fad、dadd
- 减法指令:isub、lsub、fsub、dsub
- 求余指令:irem、lrem、frem、drem
类型转换指令
类型转换指令可以在java虚拟机数值类型之间相互转换。
常见的指令:
- int 到 long:i2l
- f 到 double:f2d
对象的创建和操作
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的直接码指令。
常见的指令:
- 创建类实例的指令:new
- 创建数组的指令:newarray、anewarray、multianewarray
- 取数组长度的指令:arraylength
方法调用
以下5条指令用于方法调用
- invokevirtual:用于调用对象的实例方法
- invokeinterface:用于调用接口的方法
- invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokestatic:用于调用命名类中的类方法
- invokedynamic:用于调用以绑定了invokedynamic指令的调用点对象作为目标的方法。
总结:以上主要介绍了Java虚拟机的结构,简要的说明了每一部分的作用。希望大家有个直观的认识,接下来的博文中还会介绍下Java虚拟机编译器、Class文件格式和Java类的加载、链接和初始化,愿大家共同进步!!