JVM8(4)java虚拟机内部结构

xiaoxiao2021-02-28  11

JVM规范描述的是一种抽象化的虚拟机的行为,而不是任何一种广泛使用的虚拟机实现。 要去“正确地”实现一台Java虚拟机,其实并不像大多数人所想的那样高深和困难——只需要正确读取class文件中每一条字节码指令,并且能正确执行这些指令所蕴含的操作即可。 所有在虚拟机规范之中没有明确描述的实现细节,都不应成为虚拟机设计者发挥创造性的牵绊,设计者可以完全自主决定所有规范中不曾描述的虚拟机内部细节, 例如,运行时数据区的内存如何布局,选用哪种垃圾收集算法,是否要对虚拟机字节码指令进行一些内部优化操作(如使用即时编译器把字节码编译为机器码)。

1 Class文件格式

编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式称为class文件格式。 class文件格式中精确地定义了类与接口的表示形式,包括在平台相关 的目标文件格式中一些细节上的惯例,例如字节序( byte ordering)等。

2 数据类型

与Java程序语言中的数据类型相似,Java虚拟机可以操作的数据类型可分为两类:原始类型(primitivetype,也经常翻译为原生类型或者基本类型)和引用类型(reference type)。 与之对应,也存在原始值( primitive value)和引用值(reference value)两种类型的数值,它们可用于变量赋值、参数传递、方法返回和运算操作。 Java虚拟机是直接支持对象的。这里的对象可以是指动态分配的个类的实例,也可以指某个数组。虚拟机中使用reference类型来表示对某个对象的引用。关于reference类型的值,你可以想象成指向对象的指针。每一个对象都可能存在多个指向它的引用,对象的操作、传递和检查都通过引用它的reference类型的数据来进行。

3 原始类型与值

3.1 数值类型,boolean类型和returnAddress类型

Java虚拟机所支持的原始数据类型包括数值类型(numeric type)、boolean类型和returnAddress类型三类。 数值类型又分为整数类型(integral type)和浮点类型(floating-point type,)两种。 整数类型包括: 口byte类型:值为8位有符号二进制补码整数,默认值为零。 口short类型:值为16位有符号二进制补码整数,默认值为零。 口int类型:值为32位有符号二进制补码整数,默认值为零。 口long类型:值为64位有符号二进制补码整数,默认值为零。 口char类型:值为使用16位无符号整数表示的Unicode码点,以UTF-16编码,默认值为Unicode的null码点( ‘\u0000’)。 浮点类型包括: 口float类型:单精度 口double类型:值为双精度浮点数集合中的元素 boolean类型的值为布尔值true和false,默认值为false。 returnAddress类型是指向某个操作码(opcode)的指针,此操作码与Java虚拟机指令相对应。在虚拟机支持的所有原始类型中,只有returnAddress类型是不能直接与Java语言的数据类型相对应的。

3.2 整数类型的取值范围

Java虚拟机中的整数类型的取值范围如下: 口对于byte类型,取值范围是-128~127 包括-128和127 口对于short类型-2’5~ 2’5-1 口对于int类型,取值范围是-2’31~ 2’31-1 口对于long类型,取值范围是-2’63~ 2’63-1 口对于char类型,取值范围是O~65535,包括O和65535

3.3 returnAddress类型和值

returnAddress类型会被Java虚拟机的jsr、ret和jsr_w指令。所使用参returnAddress类型的值指向一条虚拟机指令的操作码。与前面介 绍的那些数值类的原生类型不同,returnAddress类型在Java语言之中并不存在相应的 类型,而且也无法在程序运行期间更改。

3.4 boolean类型

虽然Java虚拟机定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替。 Java虚拟机直接支持boolean类型的数组。 在Oracle公司的虚拟机实现里,Java语言中的boolean数组将会被编码成Java虚拟机 的byte数组,每个boolean元素占8位。 Java虚拟机会把boolean数组元素中的true值采用1来表示,false值采用0来表 示,当Java编译器把Java语言中的boolean类型值映射为Java虚拟机的int类型值时, 也必须采用上述表示方式。

2.4 引用类型与值

Java虚拟机中有三种引用类型:类类型(class type)、数组类型(array type)和接口类型( interface type)。这些引用类型的值分别指向动态创建的类实例、数组实例和实现了某个接口的类实例或数组实例。 在引用类型的值中还有一个特殊的值:null,当一个引用不指向任何对象的时候,它的值就用null来表示。一个为null的引用,起初并不具备任何实际的运行期类型,但是 它可转型为任意的引用类型。引用类型的默认值就是null。 Java虚拟机规范并没有规定null在虚拟机实现中应当怎样用编码来表示。

5运行时数据区

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

5.1 pc寄存器

Java虚拟机可以支持多条线程同时执行,每一条Java虚拟机线程都有自己的pc(program counter)寄存器。 在任意时刻,一条Java虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法(current method)。如果这个方法不是native的,那pc寄存器就保存Java虚拟机正在执行的字节码指令的地址, 如果该方法是native的,那pc寄存器的值是undefined。

5.2 Java虚拟机栈

每一条Java虚拟机线程都有自己私有的Java虚拟机栈(Java Virtual Machine stack),这个栈与线程同时创建,用于存储栈帧(Frame)。 Java虚拟机栈用于存储局部变量与一些尚未算好的结果。另外,它在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的出栈和入栈之外,Java虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆(应该是java虚拟机栈)中分配,Java虚拟机栈所使用的内存不需要保证 是连续的。

Java虚拟机规范既允许Java虚拟机栈被实现成固定大小,也允许据计算动态来扩展和收缩。如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。 可能的异常: StackOverflowError异常。 个OutOfMemoryError异常。

5.3 Java堆

在Java虚拟机中,堆(heap)是可供各个线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。 Java堆在虚拟机启动的时候就被创建,它存储了gc垃圾收集器)所管理的各种对象, 受管理的对象无需也无法显式地销毁。 java堆的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存不需要保证是连续的。 可能异常: OutOfMemoryError异常。

5.4方法区

在Java虚拟机中,方法区(methodarea)是可供各个线程共享的运行时内存区域。 它存储了每一个类的结构信息,例如, **运行时常量池( runtime constant pool)、 字段和方法数据、 构造函数和普通方法的字节码内容, 还包括一些 在类、实例、接口初始化时用到的特殊方法)**。

方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选挥在这个区域不实现垃圾收集与压缩。方法区在实际内存空间中可以是不连续的。 方法区可能发生如下异常情况: OutOfMemoryError异常。

5.5运行时常量池

运行时常量池( runtime constantpool)是class文件中每一个类或接口的常量池表(constant_pool table)的运行时表示形式, 它包括了若干种不同的常量。 每一个运行时常量池都在Java虚拟机的方法区中分配,在加载粪和接口到虚拟机后,就创建对应的运行时常量池。 可能异常情况: 个OutOfMemoryError异常。

5.6本地方法栈

Java虚拟机实现可能会使用到传统的栈(通常称为Cstack)来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(native method stack)。 可能异常情况: StackOverflowError异常,OutOfMemoryError异常。

6栈帧

栈帧(frame)是用来存储数据和部分过程结果,用于支持虚拟机进行方法调用和方法执行的数据结构,同时也用来处理动态链接 ( dynamic linking)、方法返回值和异常分派(dispatch exception)。 栈帧随着方法调用而创建,随着方法结束而销毁 栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。 第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地变量表和操作数栈的容量在编译期确定,并通过相关方法的code属性保存及提供给栈帧使用。因此,栈帧数据结构的大小仅仅取决于Java虚拟机的实现。 对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的局部变量表和操作数栈所进行的操作。 栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一个线程的栈帧。

6.1 局部变量表

每个栈帧内部都包含一组称为局部变量表的变量列表 编译期存储于类或接口的二进制表示之中,即通过方法的code属性保存及提供给栈帧使用。 一个局部变量可以保存一个类型为boolean、byte、char,short、int、float、reference或returnAddress的数据。 两个局部变量可以保存一个类型为long或double的数据。

6.2操作数栈

每个栈帧内部都包含一个称为操作数栈的后进先出(Last-In-First-Out,LIFO)栈。 栈帧中操作数栈的最大深度由编译期决定,并且通过方法的code属性保存及提供给栈帧使用。 栈帧在刚刚创建时,操作数栈是空的。 Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈 取走数据、操作数据以及把操作结果重新人栈。 在调用方法时,操作数栈也用来准备调用方法的参数以及接收方法返回结果。 例如,iadd字节码指令的作用是将两个int类型的数值相加, 它要求在执行之前操作数栈的栈顶已经存在两个由前面的其他指令所放人的int类型数值。 在执行iadd指令时,两个int类型数值从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算( subcomputation)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。 操作数栈的每个位置上可以保存一个Java虚拟机中定义的任意数据类型的值,包括long和double类型。

在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据 会占用两个单位的栈深度,其他数据类型则会占用一个单位的栈深度。

6.3动态链接

每个栈帧内部都包含一个指向当前方法所在类型的运行时常量池)的引用,以便对当前方法的代码实现动态链接。 在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用( symbolic reference)未表示,动态链 接的作用就是将这些以符号引用所表示的方法转换为对实际方法的直接引用

7 对象的表示

Java应拟机规范不强制规定对象的内部结构应当如何表示。 在Oracle的某些Java虚拟机实现中,指向对象实例的引用是一个指向句柄的指针, 这个句柄叉包含了两个指针,其中一个指针指向一张表格, 此表格包含该对象的各个方法,还包含指向Class对象的指针,那个Class对象用来表示该对象的类型。 句柄的另外一个指针指向分配在堆中的对象实例数据。

转载请注明原文地址: https://www.6miu.com/read-450292.html

最新回复(0)