Java虚拟机的体系结构

时间:2021-11-09 10:46:14

Java虚拟机的体系结构

在Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型、指令这几个术语描述的。

这些组成部分一起展示了抽象的虚拟机的内部抽象体系结构。


每个Java虚拟机都有一个类装载器子系统,它根据给定的全限定名来装入类型(类或接口)。

每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。


当Java虚拟机运行一个程序时,它需要内存来存储许多东西,例如,字节码,从已装载的class文件中得到的其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等。Java虚拟机会把这些东西组织到几个"运行时数据区"中,以便于管理。


某些运行时数据区是由程序中所有线程共享的,有些则是只能由一个线程拥有。

每个Java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有线程共享的。

当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,并把这些类型信息放到方法区中。

当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。

Java虚拟机的体系结构


当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个Java栈。

如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指示下一条将被执行的指令,

Java栈存储该线程中Java方法调用的状态--包括它的局部变量、被调用时传进来的参数、它的返回值,以及运算的中间结果等等。

本地方法调用的状态,依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。

Java虚拟机的体系结构

Java栈是由许多栈帧组成的,一个栈帧包含一个Java方法调用的状态。

当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中;

当该方法返回时,这个栈帧被从Java栈中弹出并抛弃。


Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。

这样可以保持Java虚拟机的指令集尽量紧凑,也有助于某些虚拟机实现的动态编译器和即时编译器的代码优化。

Java虚拟机的体系结构

Java虚拟机为每一个线程创建的内存区,这些内存区域是私有的。

任何线程都不能访问另一个线程的PC寄存器或者Java栈。


Java虚拟机的体系结构

数据类型

Java语言中的所有基本类型同样都是Java虚拟机中的基本类型。

但是指令集对boolean只有很有限的支持。

当编译器把Java源码编译为字节码时,它会用int或byte表示boolean。


returnAddress,被用来实现Java程序中的finally字句。

 

引用,是对动态创建对象的引用。

类类型的值是对类实例的引用;

数组类型的值是对数组对象的引用,在Java虚拟机中,数组是个真正的对象;

接口类型的值,则是对实现了该接口的某个类实例的引用。

还有种特殊的引用值是null,它表示该引用变量没有运用任何对象。

Java虚拟机的体系结构

字长的考量

在Java虚拟机中,最基本的数据单元是字(word),它的大小是由每个虚拟机实现的设计者来决定的。

字长必须足够大,至少是一个子单元就足以持有byte、short、int、char、float、returnAddress或reference类型的值,

而两个字单元就足以持有long或者double类型的值。

因此,虚拟机实现的设计者至少得选择32位作为字长,或者选择更为高效的字长大小。

通常根据底层主机平台的指针长度来选择字长。


在Java虚拟机规范中,关于运行时数据区的大部分内容,都是基于“字”这个抽象概念的。

比如,关于栈帧的两个部分--局部变量和操作数栈--都是按照“字”来定义的。

这些内存区域能够容纳任何虚拟机数据类型的值,当把这些值放到局部变量或者操作数栈中时,它将占用一个或两个字单元。


在运行时,Java程序无法侦测到底层虚拟机的字长大小;

同样,虚拟机的字长大小也不会影响程序的行为--它仅仅是虚拟机实现的内部属性。


类装载器子系统

在Java虚拟机中,负责查找并装载类型的那部分被称为类装载器子系统。

Java虚拟机有两种类装载器:启动类装载器+用户自定义类装载器。

启动类装载器是Java虚拟机实现的一部分,用户自定义类装载器则是Java程序的一部分。


类装载器子系统涉及Java虚拟机的其他几个组成部分,以及几个来自java.lang库的类。

比如,用户自定义的类装载器是普通的Java对象,它的类必须派生自java.lang.ClassLoader类。

ClassLoader中定义的方法为程序提供了访问类装载器机制的接口,

此外,对于每一个被装载的类型,Java虚拟机都会为他创建一个java.lang.Class类的实例来代表该类型。

和所有其他对象一样,用户自定义的类装载器以及class类的实例都放在内存中的堆区,而装载的类型信息则都位于方法区。


装载、连接以及初始化

类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按以下顺序进行:

1)装载----查找并装载类型的二进制数据

2)连接----执行验证,准备,以及解析(可选)

验证 确保被导入类型的正确性

准备 为类变量分配内存,将其初始化为默认值

解析 为类型中的符号引用转换为直接引用

3)初始化----把类变量初始化为正确初始值


启动类装载器 只要是符合Java class文件格式的二进制文件,Java虚拟机实现都必须能够从中辨别并装载其中的类和接口。

某些虚拟机实现也可以识别其他的非规范的二进制格式文件,但它必须能够辨别class文件。


每个Java虚拟机实现都必须有一个启动类装载器,它知道怎么装载受信任的类,比如Java API的class文件。

Java虚拟机规范并未规定启动类装载器如何去寻找class文件,这又是一件保留给具体的实现设计者去决定的事情。


只要给定某个类型的全限定名,启动类装载器就必须能够以某种方式得到定义该类型的数据。


用户自定义类装载器 是Java程序的一部分,但类ClassLoader中的四个方法是通往Java虚拟机的通道:

defineClass

findSystemClass

resolveClass


任何Java虚拟机实现都必须把这些方法连到内部的类装载器子系统

每个Java虚拟机实现都必须保证ClassLoader类的defineClass()方法能够把新类型导入到方法区中。

任何Java虚拟机实现都必须保证findSystemClass()能够以这种方式调用启动类装载器或者系统类装载器

resolveClass() 将对class实例表示的类型执行连接动作,

defineClass()只负责装载,当方法返回一个Class实例时,也就表示指定的class文件已经被找到并装载到方法区了,但却不一定被连接和初始化。Java虚拟机实现必须保证ClassLoader类的resolveClass方法能够让类装载器子系统执行连接操作。

命名空间

每个类装载器都有自己的命名空间,其中维护着由它装载的类型。

所以一个Java程序可以多次装载具有同一个全限定名的多个类型。

当多个类加载器都装载了同名的类型时,为了唯一地标识该类型,还要再类型名称前面加上装载该类型的类装载器标识。


Java虚拟机中的命名空间,其实就是解析过程的结果。

对于每一个被装载的类型,Java虚拟机都会记录装载它的类装载器。

当虚拟机解析一个类到另一个类的符号引用时,它需要被引用类的类装载器。

方法区

在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。

当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件(一个线性二进制数据流),然后将它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区中。该类型中的类(静态)变量同样也是存储在方法区中。


Java虚拟机在内部如何存储类型信息,这是由具体实现的设计者来决定的。

比如,在class文件中,多字节值总是以高位在前(即代表较大数的字节在前)的顺序存储。

但是,当这些数据被引入到方法区后,虚拟机可以以任何方式存储它。

假设某个实现是运行在低位优先的处理器上,那么它很可能会把字节值以低位优先的顺序存储到方法区。


当虚拟机运行Java程序时,它会查找使用存储在方法区中的类型信息。

设计者应当为类型信息的内部表示设计适当的数据结构,以尽可能在保持虚拟机小巧紧凑的同时加快程序的运行效率。


方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整;

方法区也不必是连续的,方法区可以在一个堆中*分配。

方法区也可以被垃圾收集。


类型信息

对每个装载的类型,虚拟机都会在方法区中存储以下类型信息: 1.这个类型的全限定名 2.这个类型的直接超类的全限定名 3.这个类型是类类型还是接口类型 4.这个类型的访问修饰符(public、abstract或final的某个子集) 5.任何直接超接口的全限定名的有序列表
除了上面列出的基本信息外,虚拟机还得为每个被装载的类型存储以下信息: 1.该类型的常量池 2.字段信息 3.方法信息 4.除了常量以外的所有类(静态)变量 5.一个到类ClassLoader的引用 6.一个到Class类的引用

虚拟机必须为每个装载的类型维护一个常量池,是该类型所用常量的一个有序集合,

包括直接常量(string、integer、floating point)和对其他类型、字段和方法的符号引用。

池中的数据项就像数组一样是通过索引访问的。

常量池存储了相应类型所用到的所有类型、字段和方法的符号引用


字段信息

对于类型中的声明的每一个字段,方法区中必须保存下面的信息,这些字段在类或者接口中的声明顺序也必须保存。

字段名

字段类型

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


方法信息

对于类型中声明的每一个方法,方法区中必须保存下面的信息,这些方法在类或者接口中的声明顺序也必须保存。

方法名

方法的返回类型(或void)

方法参数的数量和类型(按声明顺序)

方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)


除上面的清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下列信息:

方法的字节码(bytecodes)

操作数栈和该方法的栈帧中的局部变量区的大小

异常表


类(静态)变量,由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。

这些变量只与类有关--而非类的实例,因此类(静态)变量作为类型信息的一部分而存储在方法区中。

除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。


编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量),

都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。

作为常量池或字节码流的一部分,编译时常量保存在方法区中。

一般的类变量作为声明它们的类型的一部分数据而保存,编译时常量作为使用它们的类型的一部分而保存。