1 线程独享数据区

1.1 程序计数器

程序计数器用来记录正在执行的虚拟机字节码指令的地址。

程序技术器是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。 字节码解释器工作时通过程序计数器的值来选取下一条需要执行的字节码指令。
由于多线程通过线程轮流切换、分配处理器执行时间的实现的,在任何一个确定时刻,一个CPU核心只会执行一条线程中的指令。因此为了线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,线程间计数器私有互不影响。
如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是本地(Native)方法,这个计数器值则为空(Undefined)。该内存区域无OutOfMemoryError情况。

1.2 Java虚拟机栈

每个方法被执行的时候,JVM会同步创建一个栈帧(Stack Frame)用来存储:局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

image.png
每一个栈帧都包含了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在源码编译时,栈帧中需要多大的局部变量表,需要多深的操组数栈就已经被分析计算出来写入到方发表的Code属性中。一个栈帧需要分配多少内存,并不会受到程序运行期间数据的影响,仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。
一个线程中的方法调用链可能会很长,以程序角度来看,同一时刻、同一线程里面,在调用栈帧的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,被称为“当前栈帧”(Current Stack Frame),与这个栈帧关联的方法被称为“当前方法”(Current Method)。执行引擎所运行的所有字节码指令都指针对当前栈帧操作。

1.2.1 局部变量表

局部变量表(Local Variables Table)是一组变量值的存储空间,用来存放方法参数方法内部定义的局部变量。在java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

变量槽

局部变量表的容量以变量槽(Varaible)为最小单位。虚拟机规范中没有明确指出一个变量槽应占用的内存空间大小,只是想到性的说到每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这八种数据类型都可以使用32位或者更小的物理内存来存储。但同时它允许变量槽长度可以随着处理器、操作系统或虚拟机的实现不同而发生变化。及时64位虚拟机使用了64位的物理内存空间去实现一个变量槽,虚拟机仍要使用对其或补白的手段让变量槽的外观看起来与32位虚拟机中的一致。对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。Java语言中明确的64位的数据类型只有long和double两种。

  • reference类型:表示一个对象实例的引用。通过该引用能够做到:1、根据引用直接或间接的查找到对象在堆中的数据存放的起始地址或索引。2.根据引用直接或间接的查找到对象所属数据类型在方法区中的存储的类型信息。
  • returnAddress:指向一条字节码指令的地址
    局部变量表的使用
    虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的变量槽数量。如果访问32位数据类型的变量,索引N就代表了第N个变量槽;如果访问的是64位数据类型的变量,则会同时使用N和N+1两个变量槽。
    当一个方法被调用时,JVM会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(static修饰),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过this关键字访问。其余参数按照参数表顺序排列,占用从1开始的局部变量槽;参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。

1.2.2 操作数栈

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO) 栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项 之中。操作数栈中元素可以是包括double和long在内的任意Java数据类型。32位数据类型栈容量为1,64位数据类型占据栈容量2.

  1. 当一个方法开始执行时,该方法的操作数栈为空;在方法的执行过程中,会有各种字节码指令往操作数栈中写入或提取内容,也就是出栈和入栈的操作。比如在做算数运算时,通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的。又比如在调用其他方法时是通过操作操作数栈来进行方法参数的传递。例如,整数加法的字节码指令iadd,这条指令在运行的时候要求操组数栈中最接近栈顶的两个元素已经存入两个int型数值,当执行iadd指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。

1.2.3 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

1.2.4 方法返回地址

当一个方法开始执行后,只有两种方式退出这个方法:
第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法称为“正常调用完成”。
第二种退出方式是方法执行过程中遇到了异常,并且异常没有在方法体内得到妥善处理,就会导致方法退出。这种退出方法称为“异常调用完成”。一个方法使用异常完成出口的方式退出,是不会给上层调用者提供任何返回值。

无论采用何种退出方式,在方法退出后都必须返回到最初方法被调用时的位置,程序才能继续执行。方法退出的过程实际上等同把当前栈帧出栈,因此退出时可能执行的操作有:回复上层方法局部变量表和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈中,调整PC计数器的值指向方法调用指令后面的一条指令等。

1.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。

2 线程共享数据区

2.1 Java堆

堆内存区域是被所有线程共享的一块内存区域,在虚拟机启动时创建,用来存放对象实例。Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩 展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

2.2 方法区

方法区用来存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

2.2.1 运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。