java虚拟机内存分布
Java虚拟机在执行java程序的过程中会把它所管理的内存划分为如下若干个不同的数据区域。
1.程序计数器
程序计数器是线程私有的,它占用的空间相对较小,用来记录当前线程字节码执行到哪一步。字节码解释器通过改变这个计数器的值来获取当前线程下一步需要执行的指令,以此来确保在多条线程相互切换之后还能正确的回到切换之前执行的那一步指令。
需要注意的是如果当前线程正在执行的是一个Java方法,那么程序计数器记录的就是正在执行的虚拟机字节码指令的地址;如果当前线程正在执行的是本地(Native)方法,那此时程序计数器的值为空(Undefined)。
并且程序计数器所在的内存区域是在《java虚拟机规范》中唯一一个没有规定任何OutOfMemory Error情况的区域。
2.堆
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。Java堆的目的就是为了存放Java对象,几乎所有的Java对象实例都在Java堆中分配内存。java堆是垃圾收集器工作的主要区域。
Java堆既可以被设置为固定大小,也可以被设置为可扩展的(通过参数-Xmx和-Xms设定)。如果在堆中没法完成实例分配,且堆也不能在扩展时,就会抛出OutOfMemoryError异常。
- “-Xmx” 用于设置堆的起始内存
- “-Xms” 用于设置堆的最大内存
3.方法区
方法区(Method Area) 与Java堆一样是线程共享的区域,它用于存储已被Java虚拟机加载的类型信息、常量、静态变量。即时编译器编译后的代码缓存等数据。
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
3.1运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,如String类的 intern()方法。
当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
4.虚拟机栈
虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期和线程一样。虚拟机栈是描述java方法执行的线程内存模型,每个方法被执行是都会创建一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用到执行完毕的过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存时,会抛出OutOfMemoryError异常。
4.1栈帧
栈帧包括局部变量表、操作数栈、动态链接、方法出口等。
-
局部变量表(LocalVariables)
用来存放编译器可知的各种Java虚拟机数据类型,包括基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始 地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,64为的long和double类型的数据会占用两个槽,其余的数据类型都只占用一个槽。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索 引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐 含的参数。
在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方 法所需分配的局部变量表的最大容量。
局部变量表所需的空间在编译期间完成分配,在进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在运行期间不会改变局部变量大小(即槽的数量)。这其中还包含了槽的复用问题。
public void Test(int a){ if(a<0){ String b=""; //。。。。 } int c=0; //..... }
就以上这个Test方法来说,代表本方法的this变量占一个槽位,int类型的变量a占一个槽位,String类型的变量b占一个槽位,int类型的c占一个槽位,那么总共应该是有四个槽位,也就是局部变量表的最大槽数应该为4,但是我们看他的槽位显示只有三个。
我们再来看他的槽位详细信息,
可以看到确实只有三个,分别是this变量,a和c。在我们的代码中明明定义了String类型的b变量,但是在槽位中却没有体现,这其实就是槽位的复用问题。对于作用域比较小的变量,在他完成自己的使命后,若是之后还有变量需要使用槽位,就会使用这个已经完成使命的变量的槽位,所以这里的第三个槽位中的变量是c而不是b。
- 操作数栈
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO) 栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项 之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种 字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。如整数加法的字节码指令iadd,这条指令在运行的时候要 求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int 值出栈并相加,然后将相加的结果重新入栈。
-
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(DynamicLinking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态链接。
-
方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法 返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用 者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种 退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。
另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处 理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方 法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用 完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它 的上层调用者提供任何返回值的。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继 续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。 一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这 个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存 这部分信息。
5.本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规 定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接 就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失 败时分别抛出StackOverflowError和OutOfMemoryError异常。