Java虚拟机结构

时间:2022-12-26 22:11:37

数据类型

    Java虚拟机可以操作的数据类型分为两类: 原始类型(Primitive Types)和引用类型(Reference Type)。这两种类型的值用于变量赋值、参数传递、方法返回和运算操作
    Java虚拟机希望尽可能多的类型检查能在程序运行之前完成,亦即编译器应当在编译期间尽最大努力完成可能的类型检查,使得虚拟机在运行期间无需进行这些操作。虚拟机的字节码指令本身就可以确定操作数的数值类型。例如iadd,ladd,fadd,dadd。
    虚拟机直接支持对象,这里的对象是指动态分配的某个类的实例,也可以指定某个数组的实例,虚拟机中使用reference类型来表示对某个对象的引用。reference类型的值可以想象成类似一个指向对象的指针。对象的操作,传递和检查都通过它的reference类型的数据进行操作。

原始类型与值

    Java虚拟机所支持的原始数据类型包括了数值类型(Numeric Types)、布尔类型(Boolean Type)和returnAddress类型三类。
整数类型:
    byte类型:值为8位有符号二进制补码整数,默认为0。
    short类型:值为16位有符号二进制补码整数,默认为0。
    int类型:值为32位有符号二进制补码整数,默认值为0。
    long类型:值为64位有符号二进制补码整数,默认为0
    char类型:值为16位无符号整数表示的,指向多文本平面的Unicode值,以UTF-16编码,默认为unicode的null值。
浮点类型
    float类型:值为单精度浮点数集合中的元素
    double类型:取值范围为双精度浮点数集合中的元素
布尔类型
    boolean类型:取值范围为true或false,默认为false。在Java虚拟集中没有任何供boolean值专用的字节码指令,在Java语言中涉及到boolean类型的运算,在编译后都使用Java虚拟机中的int数据类型来替代。JVM直接支持boolean类型的数组,newarray指令可以创建此类数组,boolean类型数组的访问和修改共用byte类型数组的baload和bastore指令。

returnAddress类型,

    表示一条字节指令的操作码(Opcode),在所有虚拟机支持的原始类型中,只有returnAddress类型是不能直接与Java语言的数据类型对应起来。此类型会被Java虚拟机的jsr,ret和jsr_w指令所使用。

引用类型与值

    Java虚拟机有三种引用类型,类类型(Class Type),数组类型(Array Type)和接口类型(Interface Type)。这些引用类型的值分别由类实例、数组实例和实现某个接口的类实例或数组实例动态创建。
    数组类型还包括一个单一维度的组件类型(Componet Type),一个数组的组件类型也可以是数组。数组的元素类型(最终类型)必须是原始类型、类类型或接口类型中的一种。

    引用类型的值一个特殊的值:null,当一个引用不指向任何对象的时候,它的值用null值表示。一个为null的引用,在没有上下文的情况下不具备任何实际的类型,在有具体上下文时可以转换为任意的引用类型。

运行时数据区

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

PC寄存器

    JVM支持多条线程同时执行, 每条线程都有自己的PC寄存器任意时刻,一个JVM线程只会执行一个方法的代码,被线程执行的方法称为线程的当前方法。如果此方法不是native方法,那PC寄存器保存JVM正在执行的字节码指令的地址。如果是native的,那么PC寄存器的值是undefined。PC寄存器的容量至少能保存一个returnAddress类型的数据或一个与平台相关的本地指针的值。

JVM栈

    每条Java虚拟机线程都有自己私有的JVM栈,此栈与线程同时创建,用于存储栈帧。其作用就是存储局部变量与一些过程结果的地方,且在方法调用和返回中也扮演了重要的角色。除了栈帧的出栈和入栈之外,JVM不会受其他因素影响,栈帧可以在堆中分配,Java虚拟机栈使用的内存不需要保证是连续的。
    JVM规范允许JVM栈被实现成固定大小的或根据计算动态扩展和收缩的。若采用固定大小设计,每个线程的JVM栈容量应在线程创建的时候独立选定,且JVM实现应提供给程序员调节JVM栈初始容量的手段,对于可动态扩展和收缩的实现,提供调节最大、最小容量的手段。
    JVM栈可能发生的异常情况:
    1)请求分配的栈容量超过JVM栈允许的最大容量时,JVM抛出*Error。
    2)若JVM栈可以动态扩展,且扩展的动作尝试过,仍无法申请足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的JVM栈,JVM会抛出OutOfMemoryError。

Java堆

    Java堆是供各个线程共享的运行时内存区域,也供所有类实例和数组对象分配内存的区域。
    Java对在虚拟机启动的时候被创建,它存储了被自动内存管理系统所管理的各种对象,这些受管理的对象无需,也无法显式被销毁。Java堆的容量可以是固定大小的,也可以是随程序执行的需求动态 扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存不需要保证是连续的。
    Java虚拟机实现提供给程序员调节Java堆初始容量的手段,对于可以动态扩展和收缩堆来说,提供调节其最大、最小容量的手段。若实际所需的堆超过了自动内存管理系统能提供的最大容量,会抛出OutOfMemoryError异常。

方法区

    在JVM中,方法区是供各线程共享的运行时内存区域,它存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、以及一些在类、实例、接口初始化时用到的特殊方法。
    方法区在虚拟机启动时创建,虽然方法区是堆的逻辑组成部分,但简单的JVM实现可以选择在这个区域不实现垃圾收集。此版本的JVM规范不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定大小的,也可以是动态扩展的,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。
    JVM实现提供调节方法区初始容量的手段,对于动态扩展,提供调节最大、最小容量的手段。若方法区的内存空间不能满足内存分配请求,JVM抛出OutOfMemoryError异常。

运行时常量池

    运行时常量池是每个类或接口的常量池的运行时表示形式,包含了若干不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。
    每个运行时常量池都分配在JVM的方法区之中,在类和接口加载到虚拟机后,对应的运行时常量池就被创建出来。 若在创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,JVM会抛出一个OutOfMemoryError异常。

本地方法栈

    Java虚拟机会使用传统的栈(C stacks)来支持native方法(Java以外的其他语言编写的方法)的执行,此栈就是本地方法栈。若支持本地方法栈,此栈在线程创建时按线程分配。同样JVM规范允许本地方法栈被实现固定大小或可动态扩展的,且提供给程序员调节栈初始容量的手段,对于可动态变化的,提供调节最大、最小容量的手段。
    1)若线程请求分配栈容量超过本地方法允许的最大容量,JVM抛出*Error
    2)若本地方法栈可动态扩展,且扩展已尝试过,仍然无法申请到足够的内存或在创建新的线程没有足够的内存去创建对应的本地方法栈,JVM会抛出OutOfMemoryError。

栈帧

    栈帧(Frame)用来存储数据和部分结果的数据结构,也被用来处理动态链接、方法返回值和异常分派。
    栈帧随着方法调用而创建,随着方法结束而销毁。栈帧的存储空间分配在Java虚拟机栈之中,每个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。
    局部变量表和操作数栈的容量是编译期决定的,并通过方法的Code属性保存及提供给栈帧使用,栈帧的容量仅仅取决于JVM的实现和方法调用时可被分配的内存。
    一个线程中,只有当前正在执行的那个方法的栈帧是活动的,即当前栈帧。当前栈帧对应的当前方法称为当前方法,定义当前方法的类称为当前类。对局部变量表和操作数栈的各种操作,指的是对当前栈帧的局部变量表和操作数栈进行的操作。

当前方法调用了其他方法,或当前方法执行结束,那这个方法的栈帧不再是当前栈帧。当一个新的方法被调用,新的栈帧随之会被创建,程序控制权移交给新的方法。当方法返回时,当前栈帧会传回此方法的执行结果给钱一个栈帧。方法返回之后,当前栈帧就随之被丢弃,前一个栈帧成为当前栈帧。注意,栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一个线程的栈帧。

局部变量表

    每个栈帧内部包含一组成为局部变量表的变量列表,栈帧中局部变量表的长度由编译器决定,且存储与类和接口的二进制表示之中,即通过方法的Code属性保存及提供给栈帧使用。
    一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,long和double需要两个局部变量存储,且占用两个连续的局部变量。局部变量表使用索引来定位访问,第一个索引值为0,直到局部变量表的最大容量。
    虚拟机使用局部变量表完成方法调用时的参数传递。当一个方法被调用,它的参数传递至从0开始的连续局部变量表上。当一个实例方法被调用时,第0个局部变量一定存储实例方法所在对象的引用,后续其他参数会传递至从1开始的连续的局部变量表位置上

操作数栈

    每一个栈帧都包含一个操作数栈。栈帧中操作数栈的长度由编译器决定,并且存储与类和接口的二进制表示之中,通过方法的Code属性保存并提供给栈帧使用。
    操作数栈在栈帧刚刚被创建时,操作数栈是空的。虚拟机提供一些字节代码指令从局部变量表或对象实例的字段中复制常量或变量值到操作数栈中,也提供了指令从操作数栈取走数据,操作数据和把结果数据重新如栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接受方法返回结果。
    在操作栈中,一项运算常由多个子运算嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。在操作栈中的数据必须被正确地操作:对操作栈的操作必须与操作栈栈顶的数据类型相匹配。
    在任意时刻, 操作数栈都会有一个确定的栈深度,一个long和double类型的数据会占用两个单位的深度,其他类型会占用一个单位深度

动态链接

        每个栈帧内部都包含一个指向运行时常量池的引用来 支持当前方法的代码实现动态链接。 在Class文件中,描述一个方法调用了其他方法或者访问其他成员变量是通过符号引用来表示的。动态链接的作用是将这些符号引用所表示的方法转换为实际方法的直接引用。类加载过程中,将要解析掉尚未被解析的符号引用,并且将变量访问转化为这些变量存储结果所在的运行时内存位置的正确偏移量 。由于动态链接的存在,通过后期绑定使用的其他类的方法和变量在发生变化时,不会对调用它们的方法构成影响。

方法正常调用完成

    方法正常调用完成是指在方法的执行过程中,没有任何异常抛出。正常完成的话,很可能返回一个值给调用它的方法。这种情况下, 当前栈帧承担着回复调用者状态的责任,其状态包括调用者的局部变量表、操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器, 使得调用者的代码能在被调用的方法返回并且返回值被推入调用者栈帧的操作数栈后继续正常运行

方法异常调用完成

    方法异常调用完成,指某些指令导致了Java虚拟机抛出异常,并且虚拟机抛出的异常在该方法中没有办法处理,或者在执行过程中遇到了athrow字节码指令显式抛出异常,且在方法内部没有把异常捕获,此种情况,一定不会有方法返回值给它的调用者。

初始化方法的特殊命名

    在Java虚拟机层面上,Java语言中的构造函数是以名为<init>的特殊实例初始化方法的形式出现的,<init>这个方法名是由编译器命名的,它是非法的Java方法名字,不可能通过程序编码的方式实现,实例初始化方法只能在实例的初始化期间,通过invokespecial指令调用。
    一个类或接口最多可以包含不超过一个类或接口的初始化方法<clinit>,它是不包含参数的静态方法。类或接口的初始化方法由JVM自身隐式调用,没有任何JVM字节码指令可以调用这个方法,只有在类的初始化阶段被虚拟机自身调用。

异常

    Java虚拟机中的异常使用Throwable或其子类的实例表示,抛异常的本质是程序控制权的一种及时,非局部的转换:从异常抛出的地方转换至处理异常的地方。
    绝大多数异常的产生都是由于当前线程执行的某个操作所导致的,此类异常为同步异常。异步异常是指在程序的其他地方任意方法进行的动作导致的。
    Java异常出现的三个原因:
   1)虚拟机同步检测到程序发生了非正常的执行情况,异常会进跟着在发生非正常执行情况的字节码指令之后抛出。如字节码指令蕴含的操作违反了Java语言的定义。类在加载或链接是出错,使用某些资源产生资源限制,例如使用太多的内存。
   2)athrow字节码指令被执行
   3)由于以下原因,导致了异步异常的出现:调用Thread或ThreadGroup的stop方法,Java虚拟机内部程序错误。当某线程调用了stop方法,将会影响到其他的线程或者在线程组中的所有线程,这时其他线程中出现的异常就是异步异常。
    抛出异常的动作在Java虚拟机中是一种被精确定义的程序控制权转移过程,当异常抛出、程序控制权发生转移的那一刻,所有在异常抛出的位置之前字节码指令所产生的影响是可以被观察到的,而在异常抛出位置之后的字节码指令,则应当是没有被执行过的,若虚拟机执行的代码是被优化后的代码,有些在异常出现位置之后的代码可能已经被执行了,那么这些优化过的代码必须保证它们提前执行所产生的影响对用户来说是不可见的。
    Java虚拟机执行的每一个方法都会配有零至多个异常处理器,异常处理器描述了其在方法代码中的有效作用范围,能处理的异常类型以及处理异常代码的代码所在的位置。当方法抛出异常,Java虚拟机搜索当前方法包含的各个异常处理器,若能找到可以处理该异常的异常处理器,则将代码控制权转向异常处理器中的处理异常分支之中。