Java11虚拟机规范中描述的,当执行一个程序时需要用到的几个运行时数据区域,具体的虚拟机实现可能并没有严格按照这个划分。

1 程序计数器(The Program Counter Register)

这个区域存储的是当前线程执行的指令位置,以便在线程切换时可以快速恢复。

JVM是支持多线程的,而这个多线程执行的方式可能是以下3种(参见JLS chapter 17):

  • 由多个硬件处理器直接支持
  • 单个处理器用轮流分配时间切片的方式实现
  • 多个处理器用轮流分配时间切片的方式实现

首先,第一种情况理论上只适用于虚拟机线程数量很少的情况,因为通常硬件处理器核心数量不会很多。而后面两种情况,当线程分配到的时间切片用完时,线程执行会暂停,等待下一个时间切片,当线程恢复执行时,就需要用计数器中存储的信息来定位指令执行的位置,程序计数器是每个线程一个,是线程私有的。

当虚拟机线程执行方法a时,可能会调用其它方法比如b,那么当b激活时a就会暂停,同一时间同一个虚拟机线程只会执行一个方法,如果正在执行的方法不是native的,则记录指令执行的位置,如果是native的,则值是undefined,因为native的方法是外部实现的,并不是虚拟机可以理解的字节码指令。

规范指出该区域足够大,可以处理任意大小指令地址returnAddress,并不会抛出OutOfMemoryError异常。

2 虚拟机栈(Java Virtual Machine Stacks)

最早的版本也曾命名为“Java Stack”。

当一个虚拟机线程执行一个方法时,除了用程序计数器保存指令地址外,还需要保存方法的内部变量、过程值等,这些信息就以 栈帧(stack frame) 的形式保存在虚拟机栈里面,这个区域也是 线程私有 的。

虚拟机栈的操作只有压入和弹出栈帧,所以这个区域的占用的内存不需要是连续的。

虚拟机规范允许厂商在实现虚拟机时把虚拟机栈的大小设置成固定的或者可以根据计算过程动态扩展和收缩的,虚拟机可能会提供参数,让我们调节虚拟机栈的初始大小,以及动态扩展和收缩的时的上下限等。

异常:

  • 如果虚拟机计算过程中需要用到的虚拟机栈大小大于虚拟机允许的值,则抛出StackOverflowError
  • 如果给虚拟机栈设置初始大小时,无法申请到足够的内存,或者动态扩展申请内存失败,则抛出OutOfMemoryError

2.1 栈帧(Frame)

栈帧是虚拟机栈中存储的“基本单位”,上面提到,同一个虚拟机线程在同一时间只会执行一个方法,而每个方法在调用时就会创建一个对应的栈帧,在方法退出时(正常执行结束或者被异常中断)清除栈帧。当方法a执行时,对应的栈帧fa创建并处于激活状态,而当方法a调用方法b时,会创建新的栈帧fb,此时fb激活,fa就不是激活状态了,直到方法b退出,fb被清除,fa重新激活,方法a继续执行。

栈帧通常用来存储本地变量操作数栈、以及协助动态链接方法调用等,虚拟机实现方也可以添加一些自定义的信息,比如debug信息等。

  • 本地变量(local variables):本地变量以数组的形式保存,它的大小在编译时就确定了。保存的类形可以是8个基本类型boolean,btye,short,int,long,float,double,charreference,returnAddress,其中longdouble占用两个单位槽的位置,其它的占用一个槽,占用两个槽的值可以从起始槽n处读取,但不能从n+1读取,而n+1的位置可以被写入其它内容,一旦写了值,原来n处占用两个槽的值就被破坏了。
    当调用一个方法时,参数和内部变量等都保存在local variable中,其中第0个变量指向this,其它的按出现顺序排列。class文件中的LocalVariableTable就是本地变量。

  • 操作数栈(oprand stacks):操作数栈就是用先进后出的栈的形式保存,栈的最大深度在编译时就确定了。当栈帧刚创建时,操作数栈是空的,然后在需要时通过加载指令把本地变量的值压入栈,其它的一些指令,如iadd就会弹出栈顶的两个值,相加后把结果压入栈顶。

说到iadd指令顺便提一下java中的自动类型提升:因为虚拟机中的操作指令都是用两个16进制数表示的,也就是说,上限只能有256个指令,除去保留的(202,254,255)3个指令,从0到201全部已经使用,所以对于不太常用的short,char,boolean,byte几个类型的操作,都只有bipush,sipush等压入操作,并且在压入时自动提升为int类型,后续操作再根据需要转化,所以在代码中short i=1000;short j=2000;i=i+j;这样的写法就会出错,因为i+j在操作时已经是int类型,需要进行转化i=(short)(i+j)。这里的不常用是针对相加等操作来说,这几个类型存在的意义在于,用于数组存储时,它们仍然可以节省空间,并且也有相应的baload,saload等读取操作指令。

  • 动态链接(dynamic linking):栈帧中包含对常量池的引用,以便完成动态链接。当方法中包含对其它类型或者方法等引用时,代码中保存的只是符号引用,需要通过对常量池的读取和操作来完成动态链接,这个过程可能又会触发其它类的加载等,最终让符号引用变成当前方法可以具体使用的类或者方法。

  • 方法调用:当前方法a去调用方法b时,栈帧fa会保存当前的本地变量和操作数栈等信息,当方法b执行结束时,用这些信息恢复方法a的状态继续执行,此时程序计数器会正确增加,然后跳过对方法b的调用这条指令,并且方法b返回的结果会压入fa的操作数栈。如果方法b抛出异常,则fb被清空弹出,异常由方法a接手,如果方法a也无法处理,fa也清空并弹出,直到线程中没有栈帧,或者异常被处理。

3 本地方法栈(Native Method Stacks)

本地方法栈跟虚拟机栈类似,只是它对应的是native方法,也就是非并用Java语言实现的方法。如果虚拟机本身不支持native方法,则没有这个区域。

它的大小等设置也是由虚拟机实现方决定,有的虚拟机可能把这个区域和虚拟机栈混合在一起。

跟虚拟机栈一样,可能会抛出StackOverflowError或者OutOfMemoryError

4 堆(Heap)

堆是所有线程共享的,运行时的所有类的实例和数组,都是在堆上分配的空间。堆在虚拟机启动时创建,并且是垃圾回收机制作用的主要区域,虚拟机规范没有限定如何实现垃圾回收机制。

堆的大小可以是固定的或者动态扩展和收缩的,以及可以提供参数让我们控制相应大小等,这些都由虚拟机实现厂商决定。

堆所需的空间不必是连续的。

当程序计算过程中需要的堆大小超出了限定值,则抛出OutOfMemoryError异常。

4.1 方法区(Method Area)

方法区是堆的一部分,大小分配等也由虚拟机实现方决定,但部分虚拟机实现可能不在方法区执行垃圾回收。可以理解为跟class文件相关的内容存放的方法区中,而类的实例等存在堆的其它地方。

如果方法区的大小无法满足使用要求,则抛出OutOfMemoryError异常。

4.1.1 运行时常量池(Run-Time Constant Pool)

运行时常量池则是方法区的一部分,我们在class文件中可以看到常量池的存在。每个类或者接口都对应一个常量池,虚拟机在创建类或者接口时构建运行时常量池。

如果构建运行时常量池时,方法区无法提供足够的内存空间,则抛出OutOfMemoryError异常。