您的位置:首页 > 编程语言 > Java开发

Java 虚拟机体系结构

2015-12-02 15:51 288 查看
Java源代码被编译器编译成class文件。而并不是底层操作系统可以直接执行的二进制指令(比如Windows OS的.exe文件)。因此,我们需要有一种平台可以解释class文件并运行它。而做到这一点的正是Java 虚拟机(JVM)。



实际上,JVM是一种解释执行class文件的规范技术。各 个提 供商都可以根据规范,在不同的底层平台上实现不同的JVM。



下面是JVM实现的基本结构框图。其中类装载子系统、运行时数据区、执行引擎等 是JVM的必须要解决的几大问题。



★ 类装载器子系统

在JVM中,类装载器子系统负责查找并装载Class文件。关于这部分的装载细节详见《JVM加载class文件的原理

★ 运行时数据区

当Java虚拟机运行一个程序时,需要内存在存储许多东西。比如字节码,程序创建的对象,传递的方法参数,返回值,局部变量等等。JVM会把这些东西都组织到几个“运行时数据区”中便于管理。

(1) 方法区

当JVM使用类装载器定位Class文件,并将其输入到内存中时。会提取Class文件的类型信息,并将这些信息存储到方法区中。同时放入方法取中的还有该类型中的类静态变量。下面我们具体看看需要存储哪些信息?

●该类型的全限定名。如java.io.FileOutputStream

●该类型的直接超类的全限定名。如java.io.OutputStream

●该类型是类类型还是接口类型。

●该类型的访问修饰符(public、abstract、final)。

●任何直接超接口的全限定名的有序列表。如java.io.Closeable, java.io.Flushable。

●该类型的常量池。比如所有类型、方法和字段的符号。基本数据类型的直接数值等。

●字段信息。对类型中声明的每个字段,方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺寻也必须保存。

字段名

字段的类型

字段的修饰符(public, private, protected, static, final, volatile, transient的某个子集)

●方法信息。和字段一样保存方法的相关信息。

方法名

方法的返回类型

方法的参数的数量和类型

方法的修饰符

方法的字节码

操作数栈和栈帧中局部变量的大小 (见下面Java栈的内容)

异常表

●类静态变量。这里要注意:类中的静态变量时存放在方法区中的。并不是存放在堆中某一个该类型的对象中的。也就是我们常说的“类静态变量属于类,而不属于对象”这句话的由来了。

●指向ClassLoader类的引用。任何类都需要被类装载器装入内存。如果是被用户自定义类装载器装载的,那么JVM必须在类型信息中存储对该装载器对象的引用。
●指向Class类的 引用。对于每一个被装载的类型,虚拟机都会相应的为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方 法区中的类型信息关联起来。 这就使得我们可以在程序运行时查看某个加载进内存的类的当前状态信息。也就是反射机制实现的根本。

●方法表。 为了能快速定位到类型中的某个方法。JVM对每个装载的类型都会建立一个方法表,用于存储该类型对象可以调用的方法的直接引用,这些方法就包括从超类中继 承来的。而这张表与Java动态绑定机制( 参见《java动态绑定机制实现多态 》 )的实现是密切相关的。





方法区是多线程共享的。也就是当虚拟机实例开始运行程序时,边运行边加载 进Class文 件。不同的Class文件都会提取出不同类型信息存放在方法区中。同样,方法区中不再需要运行的类型信息会被垃圾回收线程丢弃掉。右图形象的显示出了方法区的样子。



(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个或多个操作数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: