您的位置:首页 > 其它

JVM内存结构解析(一)

2015-08-30 11:30 267 查看
一、Java虚拟机所支持的原始数据类型

1、数值类型(Numeric Types):分为整型类型(Integral Types)和浮点类型(Floating-Point Types)两种

2、布尔类型(Boolean Type )

3、returnAddress类型(表示一条字节码指令的操作码(Opcode))三类。

二、Java虚拟机中有三种引用类型:

1、类类型(Class Types)

2、数组类型(Array Types)

3、接口类型(Interface Types)

这些引用类型的值分别由类实例、数组实例和实现了某个接口的类实例或数组实例动态创建。

三、运行时数据区

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

包括以下内容:

1、PC寄存器

每一条Java虚拟机线程都有自己的PC(Program Counter)寄存器

2、Java虚拟机栈

Java虚拟机栈:每一条Java虚拟机线程都有自己私有的Java虚拟机栈(Java Virtual Machine Stack)这个栈与线程同时创建,用于存储栈帧.用于存储局部变量与一些过程结果.所以栈帧可以在堆中分配,Java虚拟机栈所使用的内存不需要保证是连续的。

3、Java堆

堆(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域.在虚拟机启动的时候就被创建,它存储了被自动内存管理系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾收集器)”)所管理的各种对象,这些受管理的对象无需,也无法显式地被销毁.Java堆所使用的内存不需要保证是连续的.

4、方法区

是可供各条线程共享的运行时内存区域.它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集。方法区在实际内存空间中可以是不连续的

5、运行时常量池

是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用.

6、本地方法栈

当Java虚拟机使用其他语言(例如C语言)来实现指令集解释器时,也会使用到本地方法栈

四、栈帧

栈帧(栈帧随着方法调用而创建,随着方法结束而销毁):是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有自己的局部变量表(Local Variables)、操作数栈(Operand Stack)和指向当前方法所属的类的运行时常量池的引用.

栈帧容量的大小仅仅取决于Java虚拟机的实现和方法调用时可被分配的内存。在一条线程之中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧(Current Frame),这个栈帧对应的方法就被称为是当前方法(Current Method),定义这个方法的类就称作当前类(Current Class)。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。栈帧是线程本地私有的数据.

1、局部变量表:每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,

既通过方法的Code属性保存及提供给栈帧使用。

一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,

两个局部变量可以保存一个类型为long和double的数据。局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。long和double类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量之中较小的索引值来定位。,Java虚拟机也不要求double和long类型数据采用64位对其的方式存放在连续的局部变量中。

当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java语言中的“this”关键字)。后续的其他参数将会传递至从1开始的连续的局部变量表位置上.

2、操作数栈:每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-Out,LIFO)栈.操作数栈所属的栈帧在刚刚被创建的时候,

操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、

操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

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

3、动态链接:每一个栈帧内部都包含一个指向运行时常量池的引用来支持当前方法的代码实现动态链接(Dynamic Linking).

在Class文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用.类加载的过程中将要解析掉尚未被解析的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量。于动态链接的存在,通过晚期绑定(Late Binding)使用的其他类的方法和变量在发生变化时,将不会对调用它们的方法构成影响。

4、方法正常调用完成

方法正常调用完成是指在方法的执行过程中,没有任何异常(§2.10)被抛出。当前栈帧(§2.6)承担着回复调用者状态的责任,其状态包括调用者的局部变量表

、操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器等。使得调用者的代码能在被调用的方法返回并且返回值被推入调用者栈帧的操作数栈后继续正常地执行

5、方法异常调用完成

初始化方法的特殊命名在Java虚拟机层面上,Java语言中的构造函数在是以一个名为<init>的特殊实例初始化方法的形式出现的,<init>这个方法名称是由编译器命名的,因为它并非一个合法的Java方法名字,不可能通过程序编码的方式实现。实例初始化方法只能在实例的初始化期间,通过Java虚拟机的invokespecial指令来调用,只有在实例正在构造的时候,实例初始化方法才可以被调用访问.

一个类或者接口最多可以包含不超过一个类或接口的初始化方法,类或者接口就是通过这个方法完成初始化的,名为<clinit>方法是一个不包含参数的静态方法。

五、异常

Java虚拟机里面的异常使用Throwable或其子类的实例来表示,抛异常的本质实际上是程序控制权的一种即时的、非局部(Nonlocal)的转换——从异常抛出的地方转换至处理异常的地方。

六、同步

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。

1、方法级的同步

当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

2、同步一段指令集

当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内

部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

保证结构化锁定规则(假设T代表一条线程,M代表一个管程):

1)T在方法执行时持有管程M的次数必须与T在方法完成(包括正常和非正常完成)时释放管程M的次数相等。

2)找方法调用过程中,任何时刻都不会出现线程T释放管程M的次数比T持有管程M次数多的情况。

在同步方法调用时自动持有和释放管程的过程也被认为是在方法调用期间发生

七、JAVA虚拟机编译

1、常量、局部变量的使用和控制

Java虚拟机是基于栈架构设计的,它的大多数操作是从当前栈帧的操作数栈取出1个或多个操作数,或将结果压入操作数栈中。每调用一个方法,都会创建一个新的栈帧,

并创建对应方法所需的操作数栈和局部变量表(参见§2.6 “栈帧”)。每条线程在运行时的任意时刻,都会包含若干个由不同方法嵌套调用而产生的栈帧,当然也包括了若干个

栈帧内部的操作数栈,但是只有当前栈帧中的操作数栈才是活动的。

2、访问运行时常量池

很多数值常量,以及对象、字段和方法,都是通过当前类的运行时常量池进行访问。类型为int、long、float和double的数据,以及表示String实例的引用类型数据的访问将由ldc、ldc_w和ldc2_w指令实现。

ldc和ldc_w指令用于访问运行时常量池中的对象,包括String实例,但不包括double和long类型的值。当使用的运行时常量池的项的数目过多时(多于256个,1个字节能表示的范围),需要使用ldc_w指令取代ldc指令来访问常量池。ldc2_w 指令用于访问类型为double和long的运行时常量池项,这条指令没有非宽索引的版本.

对于整型常量,包括byte、char、short和int,如前面§3.2节所描述,将编译到代码之中,使用bipush、sipush和iconst_<i>指令进行访问。某些浮点常量也可以编译进代码,使用fconst_<f>和dconst_<d>指令进行访问。

说明: Push int constant 1000000; a larger int value uses ldc

static修饰的方法的参数是保存在局部变量表中,因不需要存自身实例的变量

3、方法调用:

对普通实例方法调用是在运行时根据对象类型进行分派的(相当于在C++中所说的“虚方法”),这类方法通过调用invokevirtual指令实现,每条invokevirtual指令都会带有一个表示

索引的参数,运行时常量池在该索引处的项为某个方法的符号引用,这个符号引用可以提供方法所在对象的类型的内部二进制名称、方法名称和方法描述符。

4、使用类实例

Java虚拟机类实例通过Java虚拟机new指令创建。之前提到过,在Java虚拟机层面,构造函数将会以一个编译器提供的以<init>命名的方法出现。这个特殊的名字的方法也被称作实例初始化方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: