第2章Java虚拟机的结构
本文档指定了一个抽象机器。它没有描述Java虚拟机的任何特定实现。
要正确实现Java虚拟机,您只需要能够读取class文件格式并正确执行其中指定的操作。不属于Java虚拟机规范的实现细节会不必要地限制实现者的创造力。例如,运行时数据区域的内存布局,使用的垃圾收集算法以及Java虚拟机指令的任何内部优化(例如,将它们转换为机器代码)由实现者自行决定。
2.1。该class文件格式
由Java虚拟机执行的编译代码使用独立于硬件和操作系统的二进制格式表示,通常(但不一定)存储在文件中,称为class文件格式。该class文件格式精确定义的类或接口,其中包括详细信息,如字节顺序理所当然在特定平台的目标文件格式可能采取的代表性。
第4章“ class文件格式” class详细介绍了文件格式。
2.2。数据类型
与Java编程语言一样,Java虚拟机也可以使用两种类型:基本类型 和引用类型。相应地,有两种值可以存储在变量中,作为参数传递,由方法返回,并对其进行操作:原始值和参考值。
java虚拟机期望几乎所有类型检查都在运行时之前完成,通常由编译器完成,而不必由Java虚拟机本身完成。原始类型的值不需要被标记或以其他方式可检查以在运行时确定它们的类型,或者与引用类型的值区分。相反,Java虚拟机的指令集使用旨在对特定类型的值进行操作的指令来区分其操作数类型。例如,*iadd*,*ladd*,*fadd*和 *dadd*都是Java虚拟机指令添加两个数值,并产生数值结果,但每个专业的操作数类型: `int`,`long`,`float`,和`double`分别。有关Java虚拟机指令集中类型支持的摘要,请参见 [§2.11.1](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.1 "2.11.1。 类型和Java虚拟机")。
Java虚拟机包含对对象的显式支持。对象是动态分配的类实例或数组。对对象的引用被认为具有Java虚拟机类型reference。类型的值reference可以被认为是指向对象的指针。可能存在多个对象的引用。始终通过类型值操作,传递和测试对象 reference。
2.3。原始类型和价值观
Java虚拟机支持的原始数据类型是数字类型, boolean
类型(§2.3.4)和returnAddress
类型(§2.3.3)。
数字类型由整数类型(§2.3.1)和浮点类型 (§2.3.2)组成。
整体类型是:
* `byte`,其值为8位有符号二进制补码整数,其默认值为零
* `short`,其值为16位有符号二进制补码整数,其默认值为零
* `int`,其值为32位有符号二进制补码整数,其默认值为零
* `long`,其值为64位有符号二进制补码整数,其默认值为零
* `char`,其值为16位无符号整数,表示基本多语言平面中的Unicode代码点,使用UTF-16编码,其默认值为空代码点(`'\u0000'`)
浮点类型是:
* `float`,其值是浮点值集的元素,或者,如果支持,则为float-extended-exponent值集,其默认值为正零
* `double`,其值是double值集的元素,或者,如果支持,则为double-extended-exponent值集,其默认值为正零
所述的值`boolean` 类型编码的真值`true`和`false`,并且缺省值是`false`。
第一版*的的Java ®虚拟机规范*并不认为 `boolean`是一个Java虚拟机类型。但是,`boolean`值在Java虚拟机中的支持有限。第二版*的的Java ®虚拟机规范* 的治疗澄清这个问题`boolean`作为一个类型。
该`returnAddress`类型的值 是指向Java虚拟机指令的操作码的指针。在原始类型中,只有`returnAddress`类型与Java编程语言类型没有直接关联。
程序计数寄存器:
每个线程都有自己的程序计数寄存器、
每个Java虚拟机线程都在执行单个方法的代码、
如果线程执行的不是 native 方法,则程序计数寄存器包含当前正在执行的Java虚拟机指令的地址。
如果线程执行的是 native 方法,则程序计数寄存器的值未定义。
PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
Java虚拟机栈
每个Java虚拟机线程都有一个私有Java虚拟机栈,与线程同时创建。
Java虚拟机栈存储帧。
Java虚拟机栈类似于传统语言的栈,例如C:它保存局部变量和部分结果,并在方法调用和返回中起作用。
由于除了push和pop帧之外,永远不会直接操作Java虚拟机栈,因此可以对堆进行堆分配。
Java虚拟机栈的内存不需要是连续的。
Heap 堆
堆在Java虚拟机线程之间共享。
堆是运行时数据区,从中分配所有类实例和数组的内存。
堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收 ; 对象永远不会被显式释放。Java虚拟机假设没有特定类型的自动存储管理系统,可以根据实现者的系统要求选择存储管理技术。堆可以具有固定大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对堆的初始大小的控制,以及如果可以动态扩展或收缩堆,则控制最大和最小堆大小。
类的对象放在heap(堆)中,所有的类对象都是通过new方法创建,创建后,在stack(栈)会创建类对象的引用(内存地址)。
一种常规用途的内存池(也在RAM(随机存取存储器 )区域),其中保存了Java对象。和栈不同:“内存堆”或“堆”最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编辑相应的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间。
JVM将所有对象的实例(即用new创建的对象)(对应于对象的引用(引用就是内存地址))的内存都分配在堆上,堆所占内存的大小由-Xmx指令和-Xms指令来调节
方法区
method(方法区)又叫静态区,存放所有的①类(class),②静态变量(static变量),③静态方法,④常量和⑤成员方法。
1.又叫静态区,跟堆一样,被所有的线程共享。
2.方法区中存放的都是在整个程序中永远唯一的元素。这也是方法区被所有的线程共享的原因。
Java虚拟机具有在所有Java虚拟机线程之间共享的*方法区域*。
方法区域类似于传统语言的编译代码的存储区域或类似于操作系统进程中的“文本”段。
它存储每类结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法.
方法区域是在虚拟机启动时创建的。虽然方法区域在逻辑上是堆的一部分,但是简单的实现可能选择不垃圾收集或压缩它。本规范未规定方法区域的位置或用于管理编译代码的策略。方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以缩小方法区域。方法区域的内存不需要是连续的。
虚拟机的体系结构:①Java栈,② 堆,③PC寄存器,④方法区,⑤本地方法栈,⑥运行常量池
本地方法栈
Native method stack(本地方法栈):保存native方法进入区域的地址。
A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.
"frame" 帧用于`存储数据和部分结果`,以及`执行动态链接`,`返回方法的值`以及`调度异常`。
帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈的元素。
栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。
每次调用方法时都会创建一个新帧。当方法调用完成时,帧将被销毁,无论该方法是正常完成还是异常退出。
jvm为每个新创建的线程都分配一个栈。栈是以帧为单位保存线程的状态。jvm对栈只进行两种操作:以帧为单位的压栈和出栈操作。
帧是从创建帧的线程的Java虚拟机栈中分配的。每个帧都有自己的局部变量数组、自己的操作数栈、以及对当前方法的类的运行时常量池的引用。
栈帧存储了方法的局部变量表,操作数栈,动态链接和方法返回地址等信息。一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈 都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现
一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧 (Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。
jvm栈保存局部变量和部分结果,并在方法调用和返回中起作用、
jvm栈里存放的栈帧、jvm栈的内存不需要是连续的、jvm栈只允许push and pop frames。
"局部变量表" Local Variables
每个帧包含一个称为局部变量的变量数组。
帧的局部变量数组的长度在编译时确定,并以类或接口的二进制表示形式提供,同时提供与帧相关的方法的代码。
单个局部变量可以包含boolean,byte,char,short,int,float,reference或returnAddress类型的值。一对局部变量可以包含long或double类型的值。
"操作数栈" Operand Stacks
每个帧包含一个后进先出(LIFO)栈,称为其操作数堆栈。
同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的 max_stacks数据项中。
"动态连接" Dynamic Linking
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
在Class文件的常量池中存有大量的 符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化 称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
动态链接将这些符号方法引用转换为具体的方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关联的存储结构中的适当偏移。
"ClassLoader" 是负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
"Native Interface" 是负责调用本地接口的。他的作用是调用不同语言的接口给JAVA用,他会在Native Method Stack中记录对应的本地方法,然后调用该方法时就通过Execution Engine加载对应的本地lib。原本多于用一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代
"Execution Engine" 是执行引擎,也叫Interpreter。Class文件被加载后,会把指令和数据信息放入内存中,Execution Engine则负责把这些命令解释给操作系统
"Runtime Data Area" 则是存放数据的,分为五部分:Stack,Heap,Method Area,PC Register,Native Method Stack。几乎所有的关于java内存方面的问题,都是集中在这块