又隔了几天没写文章了,感觉又都生疏了,最近温习了下《 Java 虚拟机规范》以及《深入理解 Java 虚拟机》,几乎每次回看都有不同的感受,理解技术最好的方式就是造轮子,刨去性能等因素,把核心的东西用简单的方式实现一遍,这样更能深刻理解其中的原理。
今天我们要做的是用 Java 语言去实现一个 Java 虚拟机(基于解释器的方式),当然不会是完整的去实现,更多的是去理解一些核心的东西,今天这篇就当做是开篇吧。
在正式去实现一个 Java 虚拟机之前,我们要先准备一些基础知识,可能会比较单调一些,首先要理解什么是虚拟机,它做了些什么事情以及其运行结构是怎么样的?
一、Java的技术体系
从广义上讲,JRbuy、Groovy 等运行在 Java 虚拟机上的语言以及相关的程序都属于 Java 技术体系中的一员。官方所定义的 Java 技术体系主要包括以下几个部分:
1、Java 程序设计语言
2、Class 文件格式
3、Java API 类库
4、Java 虚拟机
我们可以把 Java 程序设计语言、Java 虚拟机以及 Java API 类库统称为 JDK, JDK 是用于支持 Java 程序开发的最小环境。
当编写并运行一个 Java 程序时,就同时体验了四种技术,用 Java 语言编写代码并调用Java API,编译成 Class 文件,并在 Java 虚拟机中运行 Class 文件。
Java 虚拟机是整个 Java 平台的基石,是 Java 技术用以实现硬件无关与操作系统无关的关键部分,是 Java 语言生成出极小体积的编译代码的运行平台,是保障用户机器免于恶意代码损害的保护屏障。
二、Java 虚拟机运行时数据区域
Java 虚拟机可以看作是一台抽象的计算机。如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域。
根据《 Java 虚拟机规范(Java SE7)》的规定,Java 虚拟机所管理的内存将会包含以下几个运行时数据区域。
2.1 程序计数器
程序计数器(PC) 是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理以及线程恢复等基础功能都需要依赖这个计数器来完成。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,那么这个计数器则为空 (undefined)。
2.2 Java虚拟机栈
与程序计数器一样,Java 虚拟机栈也是线程私有的,虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈桢(Stack Frame), 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用开始直至执行完成的过程,都对应的一个栈帧在虚拟机栈里入栈和出栈的过程。
局部变量表存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能只是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间 (slot ),其余的类型只占用 1 个。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中,因此一个栈帧需要多大的内存,不会受到程序运行期变量数据的影响。
在 Java 虚拟机规范中,对这个区域规定了两种异常状况:
1) 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一个 StackOverflowError 异常。
2) 如果J ava 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
2.3 本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们的区别不过是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
在虚拟机规范中对本地方法栈中方法使用的语言、使用的方式与数据结构并没有强制规定,因此,具体的虚拟机可以自由实现它。与虚拟机栈一样,本地方法栈区域也会跑出 StackOverflowError 和 OutOfMemoryError 异常。
2.4 Java 堆
对于大多数引用来说, Java 堆 (Java Heap) 是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但随着 JIT 技术的发展,栈上分配等优化技术会使用这一规范渐渐变得不那么“绝对”了。
Java 堆是垃圾回收的主战场,但 Java 虚拟机规范并没有强制规定垃圾收集器,它只要求虚拟机实现必须“以某种方式”管理自己的堆空间。
根据 Java 虚拟机规范的规定, Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,即可实现成固定大小的,也可以是可扩展的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常
2.5 方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。
Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域是比较少出现的,主要原因是这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻。
由于所有线程都共享方法区,因此它们对方法区数据的访问必须设计为是线程安全的。比如,假设同时有两个线程都企图访问一个名为 Lava 的类,而这个类还没有被装载入虚拟机,那么,这时只应该有一个线程去装载它,而另一个线程只能等待。
根据 Java 虚拟机规范,当方法区无法满足内存分配需求时,将抛 OutOfMemoryError 异常。
2.6 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本,字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
Java 虚拟机对 Class 文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求。不过,一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定在编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量池放入池中,这种特性被开发人员利用的比较多的便是 String 类的 intern() 方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
小结
作为手把手教你实现 Java 虚拟机 的开篇,前面的几篇文章可能都会以虚拟机规范为主,后面,我们将会进入实际编码阶段。