1.程序计数器
线程私有的一小块内存空间,存储当前线程正在执行的Java方法的JVM指令偏移地址,即当前线程所执行的字节码的行号(说行号只是为了方便理解,实际上不是行号)。字节码解释器就是通过改变该值来选择下一条要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖该内存完成。
★ 每个线程有自己独立的程序计数器。因为JVM的多线程机制是 通过轮流切换线程并且分配线程执行时间,所以在一个确切的时间点一个处理器(如果是多核,将其看做一个内核)能够执行的命令只有一条。因此每次线程切换后(CPU执行权发生切换时),为了让处理器能恢复到该线程的正确位置,就依赖程序计数器存的地址来恢复之前的线程。各线程有独立的程序计数器,互不影响,独立存储,称之为“线程私有”内存。栈 也是线程私有内存。
一个线程在任何时刻都可能失去执行权,所以,程序计数器的内存空间必须在创建线程时就分配好。
★ 程序计数器是唯一一个JVM规范中没有规定任何OOM情况的内存区域。
因为它只需要存储当前线程执行字节码指令的偏移地址,当执行下一条指令的时候只需要改变地址值,不需要重写申请新的内存空间,所以永远不会发生内存溢出。
它的生命周期随着线程的创建而创建,随着线程的死亡而死亡。
★ 如果正在执行native方法,则该线程程序计数器为空(Ubdifined)。因为native方法都是C/C++编写,不会产生字节码,所以也不会有指令地址。
2.虚拟机栈(线程栈)
Java方法执行的内存模型,线程私有,与其对应的线程生命周期相同。主要作用是用来存局部变量。
线程栈由一个个栈帧组成。数据结构中的栈,是一种FILO的数据结构。而线程栈就是由“栈”这种数据结构维护着一个个栈帧。
何为栈帧?线程中每调用一个方法,都会给这个方法在线程栈中分配一小块方法自己的内存,就是栈帧。
JVM对栈帧的操作只有两种,压栈和出栈。每次方法调用,都会压栈,调用方法结束后出栈。
栈帧的组成部分:
1. 局部变量表:用来存局部变量(编译器可知的的基本数据类型变量的值 和 引用数据类型变量的引用)、方法参数、成员变量的引用,JVM通过索引定位的方式使用局部变量表。
局部变量表所需的内存空间在编译器完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
局部变量表的容量以槽(slot)为最小单位,32位虚拟机中可以存放一个32位以内的数据类型(boolean,byte,char,shot,int,float,reference和returnAddress八种),存64位长度的long和double型的数据会占用两个局部变量空间(Slot)。
★reference类型,对象引用,可能是一个执行对象起始地址的引用指针,也可能是指向一个代表对象的局部或其他于此对象相关的位置。
★returnAddress类型,指向一条字节码指令的地址。
2. 操作数栈:
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。
JVM把操作数栈作为它的工作区,大多数指令都要从操作数栈弹出数据,执行运算,然后压会操作数栈。
是一个后入先出的栈,数据结构为线性表。其大小、当前帧的最大栈深度在编译时就已确定。
用于存储局部变量表、操作栈、动态链接、方法出口等信息。
int a = 1;
int b = 2;
int c = a + b;
这行代码的class文件通过javap -c 反汇编成JVM指令后:
0: iconst_1 //将int类型常量1压入操作数栈
1: istore_0 //将int类型值存入局部变量0
2: iconst_2 //将int类型常量2压入操作数栈
3: istore_1 //将int类型值存入局部变量1
4: iload_0 //从局部变量0中装载int类型值
5: iload_1 //从局部变量1中装载int类型值
6: iadd //执行int类型的加法
7: istore_2 //将int类型值存入局部变量2
8: iload_2 //从局部变量2中装载int类型值
9: ireturn //从方法中返回int类型的数据
可以看出,Java方法中每一行代码,都要经过操作数栈的处理。
3. 常量池引用:正在执行的方法对应对象的常量池的引用。
4. 动态链接。
5. 返回结果地址。方法返回分两种情况,正常完成和异常完成。
如果执行过程中没有引起异常,就是正常完成。此时当前帧会携带着操作结果返回到操作树栈中,随后栈帧被销毁。
如果执行过程中JVM抛出异常,意外完成的方法执行行不会返回结果给操作树栈。
★ 编译时就确定了成员变量表的大小、操作树栈、与当前帧相关联的方法的字节码。
(成员变量不管是基本数据类型和引用数据类型都是存在堆里面的,在类创建对象的时候初始化。当该类方法被调用的时候压入栈)
(静态变量存在方法区)
在JVM规范中,对栈这个区域规定了两种异常情况:StackOverflowError异常,OutOfMemoryError异常。
StackOverflowError:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常。
OutOfMemoryError:如果虚拟机栈支持动态扩展(当前大部分虚拟机支持动态扩展,不过Java虚拟机规范也允许固定长度的虚拟机栈),且扩展时无法申请到足够内存,就会抛出OutOfMemoryError异常。
3. 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。
Java虚拟机规范中对本地方法栈中方法使用的语言、使用方式、数据结构没有强制规定,因此由具体的虚拟机自由实现。甚至有些虚拟机(Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈也会抛出StackOverflowError、OutOfMemoryError异常。
4. 堆
所有线程共享的一块内存,是大多数应用的JVM中最大一块,在JVM启动时创建。堆区唯一的作用就是存放实例,几乎所有的对象实例都是在这里分配内存。
堆内存会被垃圾回收器进一步划分,分为新生代、老年代、持久代。
根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
5. 方法区
各个线程共享的内存区域,用于存储被已经加载的元(meta)数据:类信息、常量、静态变量、即时编译器编译后的代码等数据。另外,常量池也在方法区中。
JVM规范规定,当方法区无法满足内存分配时,会抛出OutOfMemoryError异常。
方法区中,每个数据类型都对应一个常量池,
6. 运行时常量池
是方法区的一部分。
Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
每个class文件头四个字节称为Magic Number,作用是确定是是否是一个可以被JVM接受的文件;
接着的四个字节存储的是class文件的版本号;
挨着版本号的就是常量池入口。
常量池主要存放两类常量:
1. 字面量(Literal),如String, final常量值。
2. 符号引用,存放了一些与编译相关的常量,因为Java不像C++那样有连接的过程,因此字段、方法等这些符号引用在运行期就需要进行转换,以便得到真正的内存入口地址。
class文件中的常量池称为静态常量池,JVM完成class装载后,会把静态常量池加载到内存中,存放在运行时常量池。
7. 直接内存
直接内存不属于Java规范规定,属于JVM运行时数据区的一部分。Java的NIO可以使用native方法直接在JVM堆外分配内存,使用DirectByteBuffer对象作为堆外内存的引用。