仅需一篇文章,理清Java内存区域(运行时数据区)的相关知识
参考书籍:深入理解Java虚拟机(周志明)
一、程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
它是线程私有的,为了能在虚拟机多线程切换后能恢复到正确的执行位置
在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码的指令地址
- 如果正在执行的是一个本地(Native)方法,这个计数器的值应为空
这是唯一一个没有规定任何OOM(OutOfMemoryError)的内存区域。
二、Java虚拟机栈
它是线程私有的
虚拟机栈描述的是Java方法执行的线程内存模型:
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
栈帧是线程本地的私有数据,不可能在一个栈帧中引用另外一个线程的栈帧。
如图:
每一个方法被调用直至执行完毕的过程,就对应者一个栈帧在虚拟机栈中从入栈到出栈的过程(只有在栈顶的栈帧才是有效的)。
在编译期时,一个栈帧的大小就已经确定(保存在字节码文件的方法表的Code属性之中),不会在运行期改变。
这个内存区域规定了两类异常情况:
- 如果线程请求大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存将会抛出OOM(OutOfMemoryError)异常。
(一)、局部变量表
有时也被称为本地变量表
局部变量表的容量以变量槽为最小单位,并且变量槽是可重用的(某些局部变量失效后,变量槽可交其他变量使用)
Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小
但是规定long和double类型(64位)的数据会占用两个连续变量槽(以高位对齐的方式),其余数据类型占用一个(32位以内)。
存放的内容(都是编译期可知的):
- 基本数据类型(boolean、byte、char、short、int、float、long、double)
- 对象引用(reference类型)
- returnAddress类型(指向一条字节码指令的地址)
由于所需要的内存空间都是在编译期完成分配的,因此在方法运行过程中,局部变量表的大小(变量槽的数量)是不变的。
(二)、操作数栈
有时也被称为操作栈
一个32位的数值所占的栈容量为1,而64位的数值所占的栈容量为2
操作数栈所需的容量大小在编译期就可以被完全确定下来,并保存在方法表的Code属性中。
当一个方法刚刚开始执行时,其操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。操作数栈中的元素的数据类型必须与字节码指令序列严格匹配,即不能在入栈两个int类型的数值后,却把它们当做long类型的数值去操作。
Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作数栈。
大多数虚拟机的实现里会进行一些优化处理,令两个栈帧出现一部分重叠,即下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,这样做不仅节约了一部分空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无需再进行额外的参数传递。
(三)、动态连接
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
运行时常量池中的符号引用:
- 一部分会在类加载阶段或者第一次使用的时候就被转换为直接引用,这种转换被称为静态解析。
- 另外一部分会在每一次运行期间都转化为直接引用,这部分称为动态连接。
(四)、方法出口
有时被称为方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法:
-
正常调用完成
方法正常执行完毕并退出,没有遇到任何异常。根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。
-
异常调用完成
方法执行过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。只要在本方法的异常表(Class字节码文件中)中没有搜索到匹配的异常处理器,就会导致方法退出。不会给上层调用者提供任何返回值。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法调用的位置,程序才能继续执行。
- 方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值
- 方法异常退出时,返回地址是通过异常处理器表来确定的,栈帧中一般不会保存这部分信息
方法退出的过程实际上等同于把当前栈出栈,因此退出时可能执行的操作有:
- 恢复上层的局部变量表和操作数栈
- 把返回值(如果有的话)压入调用者栈帧的操作数栈中
- 调整PC计数器的值以指向方法调用指令后面的一条指令
- …
三、本地方法栈
它是线程私有的
本地方法栈和虚拟机栈所发挥的作用是非常相似的,其区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则是为虚拟机使用的本地(Native)方法服务。
本地方法:由其他语言(如C、C++ 或其他汇编语言)编写,编译成和处理器相关的代码。
本地方法保存在动态连接库中,格式是各个平台专用的。(Java方法是与平台无关的)
运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈拓展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
四、Java堆
它是线程共享的,在虚拟机启动时创建,存放对象实例以及数组(Java世界里几乎所有的对象实例都在这里分配内存)
对于Java程序来说,Java堆是虚拟机所管理的内存中最大的一块。
-
Java堆是垃圾收集器管理的内存区域。
-
Java堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
-
Java堆可以实现为固定大小,也可以是可扩展的(通过参数-Xmx和-Xms设定)。
当Java堆中没有内存进行实例分配或栈拓展失败时将会抛出OutOfMemoryError异常。
在JDK1.7之后,原本方法区中的字符串常量池就移到了Java堆中
五、方法区
它是线程共享的,用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
在JDK1.8之前,HotSpot虚拟机团队选择把收集器的分代设计拓展至方法区,那时候常将方法区称为永久代(并不等价)。这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。
缺点:这种设计会导致Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,不设置也有默认大小)。
- 在JDK1.6的时候,HotSpot虚拟机团队就计划放弃永久代,改用本地内存来实现方法区。
- 在JDK1.7的时候,原本方法区中的字符串常量池、静态变量等就被移出。
- 在JDK1.8的时候,完全废弃永久代的概念,在本地内存中实现元空间来替代,并把JDK1.7中永久代剩余的内容全部移到元空间中。
方法区除了和Java堆一样不需要连续的内存和可以选择固定大小或者可拓展外,甚至可以不选择实现垃圾收集,但数据并非永久存在。这区域的内存回收目标主要是对常量池的回收和对类型的卸载。
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
(一)、运行时常量池
运行时常量池是方法区的一部分。
Class文件中有常量池表(Class文件常量池),用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池可以由不同的虚拟机按照自己的需要来实现这个内存区域,一般来说,除了保存Class文件中描述的符号引用之外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
运行时常量池具备动态性,不要求常量一定只有在编译期才产生(不一定要在Class文件常量池中),运行期间也可以将新的常量放入池中。如String类的intern()方法(面试常考)。
当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
全文结,请大家多多支持!!!