Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自不同的用途,以及创建和销毁时间,有些区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。Java虚拟机所管理的内存将会在包括以下几个运行时数据区域,如下图所示
![2b9e542d148c5babed31545a31e10423.png](https://i-blog.csdnimg.cn/blog_migrate/a50466aff49b7e119521c1dadc3ced19.jpeg)
执行引擎是Java虚拟机最核心的组成部分之一。在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型称为各种虚拟机执行引擎的统一外观。在不同的虚拟机实现里,执行引擎在执行Java代码时肯呢过有解释执行和编译执行两种选择。但从外观看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
1.程序计算器
程序计算器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计算器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计算器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的位置,每条线程都需要有一个独立的程序计算器,各条线程之间计算器互不影响,独立存储,称这类内存区域为”线程私有“的内存。
如果内存正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是Native方法。这个计数器值则为空(undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.Java虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。它主要用于存放对象引用。
![ca6e3e67bbd82065f15965e3719eb11c.png](https://i-blog.csdnimg.cn/blog_migrate/630679eb0109cab8657732eff73588c9.jpeg)
在Java虚拟机规范中,对这个区域规定了两种异常的情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
2.1什么是栈帧?
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中虚拟机栈的栈元素。栈帧(Stack Frame)存储了局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
每一个栈帧都包含了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译代码时,栈帧中需要多大的局部变量表,多深的操作数栈有已经完全确定了,并写入方法表的Code属性中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而取决于具体的虚拟机实现。
对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个站帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作,典型的栈帧结构如下图所示:
![7740399824bd168d828c5321f6168a0e.png](https://i-blog.csdnimg.cn/blog_migrate/dac9e56dcbfad156a613a155b55d04e1.jpeg)
接下来详细讲解一下栈帧中的局部变量表、操作数栈、动态链接、方法返回地址等各个部分的作用和数据结构。
局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位。每个Slot都应该能存放一个boolean,byte,char,short,int,float,reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出“每个Slot占用32位长度的内存空间”是有一些差别的,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化,只要保证即使在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观看起来与32位虚拟机中的一致。
一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean,byte,char,short,int,float,reference和returnAddress这8种类型。reference类型表示一个对象实例的引用,虚拟机规范既没有说明它的场地,也没有明确指出这种引用是什么结构。但从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引;此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。returnAddress类型目前很少见了,现在已经由异常表代替。
对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。Java语言中64位的数据类型只有long和double。由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。对于两个相邻的共同存放一个64位的数据的两个Slot,不允许采用任何方式单独访问其中某一个,Java虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数,其余参数则按照参数表顺序排序,占用从1开始的局部变量Slot,参数分配完毕后,再根据方法体内部的变量顺序和作用域分配其余的Slot。
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的。需要注意的是,一个局部变量定义了但没有赋值是不能使用的。
操作数栈:是一个后进先出的栈,它的最大深度在编译的时候就写入到Code属性的max_stacks数据项中。操作数栈的每个元素可以是Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64为数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也即入栈/出栈操作。
操作数栈中的元素类型必须与字节码指令的序列严格匹配,在编译程序代码时编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。
另外,在概念模型中,两个栈帧作为虚拟机栈的元素是完成相互独立的,但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面 栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数传递,如图所示:
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使 用的时候就转化为直接引用,这种转化成为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分成为动态链接。
方法返回地址:当一个方法开始执行后,只有两种方式可以退出这个方法:第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候肯呢过会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
第二种是在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有找到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完 成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
附加信息:虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完成取决于具体的虚拟机实现。
3.本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也即是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。有的虚拟机(如HotSpot)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
4.Java堆
对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都是采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点有:Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都是对象的实例,进一步划分的目的是为了更好地回收内存,或更快地分配内存。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘一样。在实现时,既可以实现成固定大小的,也可以是可扩展的。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
5.方法区
方法区(Method Area)与Java堆一样,是各个线程共享的,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
在HotSpot虚拟机上开发、部署程序的开发者来说,很多人都把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代实现方法区而已。
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域比较少出现,但并非数据进入了方法区就永久的存在。这区域的内存回收目标主要是针对常量池的回收和堆类型的卸载,一般来说,这个区域的回收情况比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
当方法区无法满足内存需求时,将抛出OutOfMemoryError异常。
6.运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
Java虚拟机堆Class文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才被虚拟机认可、装载和执行,但对于运行时常量池,Java虚拟机却没有做任何细节的要求。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定要在编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入到常量池中,这种特性被开发人员利用得比较多的是String类的intern()方法。
当常量池无法再申请到内存时会抛出OutOfMeMoryError异常。
7.直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也可能导致OutOfMemoryError异常出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中能显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本地直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本地总内存大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。
我是白菜,平凡但很有营养!
System.out.println("喜欢点个关注吧!");