JVM运行时内存模型
概述
java程序在运行的时候会在内存中开辟不同的空间用以管理不用的内存区域,每个区域都有自己的功能,创建和销毁时间,有的区域在java虚拟机(下文简称JVM)启动时创建,在JVM退出时销毁;而有的区域是每个线程都有的,会随着线程的创建而创建,线程的退出而销毁。
1、虚拟机栈(JVM Stacks)
每个JVM线程都有一个私有的JVM栈,每一个栈都存储了“栈帧”,用来存放局部变量和结果,并且在方法调用和返回中扮演一个角色。因为除了存取帧以外不需要直接操作栈,帧可以由堆来进行分配,Java虚拟机栈的内存不需要是连续的。栈的大小可以固定也可以动态扩展。
- 当线程中计算所需要的内存超出栈的允许时,JVM会抛出StackOverflowError
- 如果允许动态的扩展JVM栈,但是内存不足,或者内存不足以为新线程在创建时初始化JVM栈,JVM会抛出OutOfMemoryError(下文简称OOM)
####1.1、栈帧(Frames)
栈帧用来存储数据和部分结果,以及执行动态链接(dynamic linking),返回方法的执行结果和分发异常。
每次方法执行的时候都会创建一个新的栈帧。在方法执行完以后栈帧就会被销毁,无论方法是否正常执行完毕(比如抛出了一个未捕获的异常)。当线程创建栈帧的时候,栈帧从JVM栈中被分配。每个栈帧拥有它自己的局部变量(local variables)的数组、操作数栈(operand stack)、以及当前方法所在类的运行时常量池(run-time constant pool)的引用。
栈帧可以被额外的特定于实现的信息扩展,比如debug信息。
局部变量数组和操作数栈的大小在编译时被确定,并连同与栈帧相关联的方法的代码一起被提供。因此,栈帧数据结构的大小仅仅取决于JVM的具体实现,并且可以在方法调用的同时为这些结构分配内存。
仅有一个栈帧(正在执行的方法的栈帧),在给定控制线程中的任何点处都是活动的,该帧被称为当前栈帧,并且其方法被称为当前方法,定义当前方法的类是当前类。在局部变量和操作数栈上的操作都拥有当前栈帧的引用。
如果一个栈帧的方法调用另一个方法或者它的方法已经执行完毕,那么该栈帧不再是当前栈帧。调用方法时,会创建一个新的栈帧,并在控制转移到新方法时变为当前栈帧。当方法返回时,栈帧将它的方法的执行结果返回给上一栈帧(如果有的话)。当前栈帧会被丢弃,同时上一栈帧会重新标记处为当前栈帧。
- 被一个线程创建的栈帧,是该线程的局部栈帧,不能被其它线程引用。
1.2、局部变量(Local Variables)
每一个栈帧都包含一个被称为局部变量数组的变量数组。栈帧的局部变量数组的长度在编译时确定,并且以类或者接口的二进制形式连同栈帧相关联的方法的代码一起被提供。
单个局部变量可以保存boolean,byte,char,short,int, float,reference,或returnAddress类型的值。一对局部变量可以保存一个long或double类型的值。
局部变量可以用索引来编址。第一个局部变量的索引是0。索引只能是介于零和局部变量数组的大小(0 <= i && i < localVariables.length
)之间的整数。
long或double类型的值会占据两个连续的局部变量,这样的值可能只能用更小的索引来编址,比如一个double类型的值存储在局部变量数组中索引为n的位置,实际上会占据索引为n和n+1的局部变量。然而,n+1处的局部变量是无法被加载的。n+1处的局部变量可以用来存储,但是这样会使n处的内容(上文的double)失效。
JVM没有要求n必须为偶数,直观而言,double和long类型的值在局部变量数组中不需要64位对其。JVM的实现者可以自由的决定合适的方式去表述这样的值,只要是使用为这样的值保留的2个局部变量。
在方法执行的时候,JVM使用局部变量去传递参数,在类方法调用时,使用从索引为0开始的连续的局部变量传递参数。在实例方法调用时,索引为0的局部变量始终用来传递实例方法所在的对象(就是java中的this)。随后,其它参数从索引为1的局部变量开始传递。
1.3、操作数栈(Operand Stacks)
每一个栈帧都包含一个被称之为操作数栈的后进先出的栈。栈帧的操作数栈的最大深度在编译时确定,并且连同栈帧相关联的方法的代码一起被提供。
在它被上下文清除的地方,我们一般称当前栈帧的操作数栈为操作数栈。
在栈帧创建时,它包含的操作数栈是空的。JVM提供了指令将局部变量和字段中的值以及常量加载到操作数栈中。其他JVM指令从操作数栈获取操作数,对它们进行操作,并将结果放入操作数栈,操作数堆栈还用于准备传递给方法和接收方法结果的参数。
比如,iadd指令用来相加两个int类型的值,JVM要求这2个值必须是操作数栈中最顶层的2个值,是由上一个指令放到那里的。首先这2个值会先从操作数栈中取出,然后相加,最后再将相加的和放入操作数栈。子计算可以嵌套在操作数栈上,这样值就可以被包含的计算使用。
操作数栈上的每个条目都可以存储一个任何JVM类型的值,包括double和long类型的值。
必须以适合其类型的方式去操作操作数堆栈中的值。以下情况是不可能的,比如,放入2个int类型的值,然后将他们当作long类型对待,或者放入2个long类型的值,然后使用iadd指令进行相加。少数的JVM指令(dup指令和swap指令)将运行时数据区域作为原始值进行操作,而不考虑其特定类型。这些指令以这样一种方式定义:它们不能修改或者破坏单个值。这些操作数栈的操作约束是通过类文件验证(Verification of class Files)强制执行的。
在任何时间点,操作数栈都拥有关联的深度,一个long或者double类型的值增加2单位的深度,其它类型的值增加1单位的深度。
1.4、动态链接(Dynamic Linking)
每个栈帧都包含一个关于当前方法所在类的运行时常量池的引用,这是为了支持方法代码的动态链接。类文件中的方法代码通过符号引用指向正在被执行的代码和被访问的变量。动态链接将这些符号方法引用转为具体的方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关联的存储结构中的适当偏移。
方法和变量的这种延迟绑定使得方法使用的其他类中的更改不太可能破坏此代码。
2、本地方法栈(Native Method Stacks)
JVM的实现可以使用传统的栈,俗称“C栈”,以支持native方法(用Java编程语言以外的语言编写的方法)。无法加载native 方法并且本身不依赖于传统栈的Java虚拟机实现不需要提供本机方法栈。如果提供,则通常在创建每个线程时为每个线程分配本机方法栈。
JVM的实现可以为程序员或用户提供对固定的本地方法栈的初始内存的控制,以及如果本地方法栈是可扩展的,则提供本地方法栈的最大和最小内存的控制
3、程序计数器(program counter Register)
JVM支持多个线程并发执行,每个线程都有自己的程序计数器。倘若当前执行的不是native方法,则该寄存器中保存当前执行的JVM指令的地址;倘若执行的是native 方法,则PC寄存器中为空。
4、堆(heap)
JVM中有一块在所有线程中共享的内存,叫做heap(堆)。所有的类对象和数组都在堆上进行分配。
堆在JVM启动时创建,堆中的对象由自动存储管理系统(通常被称为垃圾收集器)进行回收。java中的对象是不需要显示释放的。如果不显示指定垃圾收集器,JVM会根据系统环境自动选择合适的垃圾收集器。堆的大小可以是固定的也可以是可扩展的,如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。 JVM的实现可以为程序员或用户提供对堆的初始大小的控制,以及如果可以动态扩展或收缩堆,提供最大和最小堆大小的控制。
- 如果计算需要的堆内存超过垃圾收集器可用的堆大小,则JVM会抛出OOM错误
5、方法区(Method Area)
JVM中有一块在所有线程中共享的内存,叫做方法区。方法区类似于传统语言的编译代码的存储区域或类似于操作系统进程中的“文本”段。它用来存储类的结构信息,比如运行时常量池、字段和方法数据、以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法。
方法区在JVM启动时创建,虽然在JVM规范中,它是属于堆的逻辑部分,但是简单的JVM实现可能选择不对方法区进行垃圾收集或压缩。规范不强制方法区域的位置或用于管理编译代码的策略。方法区域可以是固定大小的,或者可以根据计算的需要进行扩展的,并且如果不需要更大的方法区,则可以缩小方法区。方法区的内存不需要是连续的。
JVM的实现可以为程序员或用户提供对方法区的初始大小的控制,以及如果可以动态扩展或收缩方法区,提供最大和最小方法区大小的控制。
- 如果方法区域中的内存无法满足分配请求,则Java虚拟机会抛出OOM错误
###6、运行时常量池(Run-Time Constant Pool)
.class文件不仅仅包含类的描述信息也包含常量表的信息,一个运行时常量池代表的就是一个类或者接口在运行时的常量表的信息。它包含多种常量,范围从编译时已知的数字字面量到必须在运行时解析的方法和字段的引用。运行时常量池提供类似于传统编程语言的符号表的功能,尽管它包含比传统的符号表更宽范围的数据。
每个运行时常量池都是从JVM的‘方法区’中分配的。当JVM创建类或接口时,将构造类或接口的运行时常量池。
- 在创建类或接口时,如果运行时常量池的构造需要的内存比Java虚拟机的方法区中可用的内存多,则Java虚拟机会抛出一个OOM错误