众所周知,Java源代码被编译器编译成class文件。而并不是底层操作系统可以直接执行的二进制指令(比如Windows OS的.exe文件)。因此,我们需要有一种平台可以解释class文件并运行它。而做到这一点的正是Java 虚拟机(JVM)。
实际上,JVM是一种解释执行class文件的规范技术。各 个提 供商都可以根据规范,在不同的底层平台上实现不同的JVM。
下面是JVM实现的基本结构框图。其中类装载子系统、运行时数据区、执行引擎等 是JVM的必须要解决的几大问题。
![]() ★ 类装载器子系统 字段名 字段的类型 字段的修饰符(public, private, protected, static, final, volatile, transient的某个子集) ●方法信息。和字段一样保存方法的相关信息。 方法名 方法的返回类型 方法的参数的数量和类型 方法的修饰符 方法的字节码 操作数栈和栈帧中局部变量的大小 (见下面Java栈的内容) 异常表
(2) 堆 Java 程序在运行时创建的所有类型对象和数组都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的对象内存空间。但是堆中开辟对象的空间并没有任何 人工 指令可以回收,而是通过JVM的垃圾回收器负责回收。
堆中对象存储的是该对象以及对象所有超类的实例数据(但不是静态数据), 比如下面的类型: class X{ private int data; private static int stcdata=0;
public X(int d){ this.data=d; } } X x1=new X(100); X x2=new X(200); 这样在堆中开辟了两个对象x1和x2的内存空间。其中x1中的一个实例数据data=100,而x2的data=200。但是这两个对象中都没有stcdata这样的数据,这个静态数据存储在上面讲到的方法区中。
此外,堆中对象还必须有指向方法区中的类信息数据(见上面方法区)。 为什么需要这个信息呢?因为当程序在运行时需要对象转型,那么JVM必须检查当前对象所属类型及父类的信息。以判断转型是否是合法的,而这一点也是instanceof操作符实现的基础。
当然,上述只是JVM的规范,具体堆的实现是由JVM设计者来决定。下面两幅图就直观的表现出了堆对象的不同实现结构: 其中一个对象的引用可能在整个运行时数据区中的很多地方存在,比如Java栈,堆,方法区等。
堆中对象还应该关联一个对象的锁数据信息以及线程的等待集合。 这些都是实现Java线程同步机制的基础。但实际上很多具体实现中并不在对象自身内部保存一个指向锁数据的指针。而只有当第一次需要加锁的时候才分配对应锁数据。另外,每个对象都会从Object中继承三个Object方法(wait、notify、notifyAll),当某个线程在一个对象上调用了等待方法时。JVM就会阻塞这个线程,并把这个线程放在该对象的等待集合中。知道另外一个线程在该对象上调用了notify/notifyAll,JVM才会在等待集合中唤醒一个或全部的等待线程(参见《正确理解线程等待和释放(wait/notify)》)。
【数组对象】 在Java中,数组也是对象,那么自然在堆中会存储数组的信息。事实也确实如此,对于JVM而言,数组与其他类对象没有任何区别。 数组也有属于的类Class,具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度是多少。 数组类的名称由两部分构成:(1)每一维用一个方括号“[”表示。(2) 用字符或字符串表示元素类型。比如一维数组对象int[] a所属类型名为"[I",二维数组对象byte[] b所属类型名为"[[B"。 下图是二维数组对象在堆中的具体实现方式:
(3) 程序计数器
对于一个运行的Java而言,每一个线程都有一个PC寄存器。当线程执行Java程序时,PC寄存器的内容总是下一条将被执行的指令地址。
(4) Java栈 - 栈帧
每启动一个线程,JVM都会为它分配一个Java栈,用于存放方法中的局部变量,操作数以及异常数据等。当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧。并将该栈帧压入Java栈中,方法执行完毕时,JVM会弹出该栈帧并释放掉。 注意,Java栈中的数据是线程私有的,一个线程是无法访问另一个线程的Java栈的数据。这也就是为什么多线程编程时,两个相同线程执行同一方法时,对方法内的局部变量时不需要数据同步的原因。
【栈帧 】 栈帧有三部分构成:局部变量区、操作数栈和帧数据区。在编译器编译Java代码时,就已经在字节码中为每个方法都设置好了局部变量区和操作数栈的数据和大小。并在JVM首次加载方法所属的Class文件时,就将这些数据放进了方法区。因此在线程调用方法时,只需要根据方法区中的局部变量区和操作数栈的大小来分配一个新的栈帧的内存大小,并堆入Java栈。
局部变量区: 用来存放方法中的所有局部变量值,包括传递的参数。这些数据会被组织成以一个字长(32bit或64bit)为单位的数组结构(以索引0开始)中。其中类型为int, float, reference(引用类型,记录对象在堆中地址)和returnAddress(一种JVM内部使用的基本类型)的值占用1个字长,而byte, char和shot会扩大成1个字长存储,long,double则使用2个字长。
操作数栈: 用来在执行指令的时候存储和使用中间结果数据。
帧数据区: 常量池的解析,正常方法返回以及异常派发机制的信息数据都存储在其中。
下图展示了addAndPrint()调用addTwoTypes()时,Java栈的变化:
★ 执行引擎 运行Java的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,他要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片级指令直接执行字节码,或者间接通过JIT执行编译过的本地代码。
指令集: 实际上,Class文件中方法的字节码流就是有JVM的指令序列构成的。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。
Java虚拟机指令集关注的中心是操作数栈和局部变量集合。我们可以看看下面一组指令在执行引擎中执行的过程:
很显然,上面的指令反复用到了Java栈中的某一个方法栈帧。实际上执行引擎运行Java字节码指令很多时候都是在不停的操作Java栈,也有的时候需要在堆中开辟对象以及运行系统的本地指令等。但是Java栈的操作要比堆中的操作要快的多,因此反复开辟对象是非常耗时的。这也是为什么Java程序优化的时候,尽量减少new对象。
下面将会是很有趣的过程,我们用一段代码来生动的展现JVM是如何运行这段程序的。
通过编译器将下面的代码编译成edu/hr/jvm/Test.class 和 edu/hr/jvm/bean/Act.class。然后开始启动JVM:
(5) JVM加载进Act.class,并提取Act类信息 放入方法区中。见上图方法区所示,然后以一个直接指向方法区Act类信息的直接引用替换开始在常量池中的符号引用"Act",这个过程就是常量池解析 。以后就可以直接访问Act的类信息了。
(6) 此时JVM可以根据方法区中的Act类信息,在堆中开辟一个Act类对象 act。见上图堆所示。
(7) 接着开始执行main方法中的第二条指令调用doMathForever。这个可以通过堆中act对象所指的方法表 中查找,然后定位到方法区中的Act类信息中的doMathForever方法字节码。在运行之前,仍然要组建一个doMathForever栈帧压入Java栈,如上图所示。(注意:JVM会根据方法区中doMathForever的字节码来创建栈帧的局部变量区和操作数栈的大小)
● 下面运行每一条指令后,看一下局部变量区和操作数栈的变化: ① 指令[iconst_0] 将int类型变量的数据0压入操作数栈。 局部变量区 操作数栈 index hex value value offset hex value value (变量i) 0 0 00000000 0 optop-> 1 ② 指令[istore_0] 弹出操作数栈顶的数据0,将结果存储在局部变量区中index=0的空间中。 局部变量区 操作数栈 index hex value value offset hex value value (变量i) 0 00000000 0 optop-> 0 1 ③指令[iinc 0 1] 把常量值1加到局部变量区中index=0的空间上。 局部变量区 操作数栈 index hex value value offset hex value value (变量i) 0 00000001 1 optop-> 0 1 ④指令[iload_0] 把局部变量区index=0中的数据堆入操作数栈。 局部变量区 操作数栈 index hex value value offset hex value value (变量i) 0 00000001 1 0 00000001 1 optop-> 1 ⑤指令[iconst_2] 把int类型变量的数据2压入操作数栈。 局部变量区 操作数栈 index hex value value offset hex value value (变量i) 0 00000001 1 0 00000001 1 1 00000002 2 optop-> ⑥指令[imul] 弹出操作数栈中的两个数据1和2,相乘之后的结果2堆入操作数栈 局部变量区 操作数栈 index hex value value offset hex value value (变量i) 0 00000001 1 optop-> 0 00000002 2 1 ⑦指令[istore_0] 弹出操作数栈顶的数据2,将结果存储在局部变量区中index=0的空间中。 局部变量区 操作数栈 index hex value value offset hex value value (变量i) 0 00000002 2 optop-> 0 1 ⑧指令[goto 2] 跳转到指令iinc 0 1处循环执行下去.....
当然,这个例子不停的执行下去只会出现算术溢出,也就是一个字长(2bytes)的整型变量i 无法表示不停计算的结果了。但是JVM不会抛出任何异常,
附:在《深入Java虚拟机》一书第5章节有一个JVM模拟运行上面程序的源代码和applet展示,做的很不错。下面是这本书的配到源代码,大家可以学习一下。 |
|