JVM在执行Jva程序时候会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间。下图表示运行时数据区的基本划分,图片来自其他微博。
1 程序计数器
程序计数器是一块比较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,通过改变这个计数器的值还选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器是线程私有的,每个线程都有自己的程序计数器并且不相互干扰。
- 当线程在执行一个Java方法的时候,程序计数器记录的是当前正在执行的虚拟机字节码指定地址
- 当正在执行的是Native方法的时候,计数器的值为空。
程序计数器是Java虚拟机规范中唯一没有规定OutOfMemoryError情况的区域。
2 Java虚拟机栈
与程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程是一样的,虚拟机栈是描述Java执行的内存模型:每当启动一个新线程的时候,java虚拟机都会为它分配一个java栈,每个方法执行的同时都会创建一个栈帧,它是Java虚拟机栈的栈元素结构。每个方法调用都是方法栈帧从入栈到出栈的过程。
虚拟机栈定义了两种异常
- StackOverflowError异常:如果线程请求的栈深度大于允许的深度,将会抛出
- OutOfMemoryError异常:如果虚拟机栈可能动态拓展,如果拓展时无法申请到足够的内存,将会抛出。
2.1 栈帧
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧储存了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。在编译程序时,栈帧需要多大的局部变量表、多深的操作数栈就已经完全确定了,并且写入方法表的Code属性中。因此一个栈帧需要的大多不会受到运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线中,只有位于栈顶的栈帧是有效的,称为当前栈帧,与这个栈帧相关联的称为当前方法。他们的关系如下:
2.2 局部变量表
局部变量表是一组变量值储存空间,用于存放方法参数和方法内部定义的局部变量。(所以我们所说的局部变量放在栈上就是从这来的)。在Java编译成Class文件时,这个方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
在局部变量表中存放了编译期可知的各种基本数据类型、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型。其中64为长度的long和double类型占用两个空间(slot),其余类型占用1个slot
虚拟机通过索引定位的方式使用局部变量表,索引范围从0开始至最大的slot数。如果访问32位变量,索引n就代表第n个slot,如果为64位,则n和n+1两个slot表示改变量。对于两个相邻的slot表示一个数据的时候,不允许单独访问其中一个。
索引0号位默认用于保存this指针,其余参数按照参数表顺序排列。
2.3 操作数栈
操作数栈是一个后进先去的栈,其最大深度在编译时已经确定。当一个方法开始执行的时候,栈为空。在方法执行过程中,字节码指令会进行入栈与出栈的操作。例如:进行两个数相加的操作时,整数加法字节码iadd在运行的时候栈顶的两个元素取出,然后相加,把相加的结果入栈。对于两个栈帧,为了进行优化,他们的操作栈可能存在重叠的区域,以实现数据共享。
2.4动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法在调用过程中的动态链接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分在类加载或第一次使用的时候转化为直接引用,这种称为静态解析,另一部分在每一次运行期间转换为直接引用,这部分称为动态链接。
2.5方法返回地址(方法出口)
当一个方法开始执行后,只有两个方式可以退出。一种是遇到任意一个返回字节码指令,这种退出称为正常完成出口;另一种是异常,只要方法的异常表中没有匹配的处理器,将会导致方法退出,这种退出称为异常完成出口。无论采用何种方式退出,都会返回到方法被调用的位置,程序才能继续,方法返回时可能需要在栈帧中保存一些信息,用来恢复它的上层方法的执行状态。方法退出的过程实际上就等于把当前栈帧出栈。
2.6附加信息
虚拟机规范允许虚拟机可以在栈帧存放额外的信息,例如与调试相关的信息。
3 本地方法栈
本地方法栈帧与虚拟机栈所发挥的作用是相似的,只不过本地方法栈为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
4 Java堆
Java堆是Java虚拟机所管理的内存中最大的一块,它是被所有线程共享的一块区域。虚拟机启动创建时,此区域唯一的作用就是存放对象实例。几乎所有的对象实例和数组都要在堆上分配。
Java堆是垃圾收集器管理的主要区域,因此很多时候也称做“GC堆”。
从内存回收的角度,现在收集器都用分代收集算法,所以Java堆中还可以细分为:新生代和老年代,在细致一点有Eden空间,From Survivor空间、To Suivivor空间等。
从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的。如果在堆中没有内存完成实例分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常。
5 方法区
方法区和Java堆一样,是各个想成共享的内存区域,它用于储存已被虚拟机记载的类信息、常量、静态变量、及时编译后的代码等数据。它有一个别名,叫“非堆”,相对于前面的新生代、老年代来说,它可以称为“永久代”。因为相对而言,垃圾回收在这个区域是比较少出现的,但也并非是数据进入这个区域就永久存在了,这个区域的内存回收主要针对常量池的回收和对类型的卸载。
5.1 运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息以外,还有一项信息是常量池,用于存在编译期声场的各种字面量和符号引用,这部分在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另一个特性是具备动态性,Java语言并不要求常量一定只有编译期才能产生,运行期也可能将新的常量放入池中。例如String类的intern()方法。
根据Java虚拟机规范中的描述,每个类和接口被加载到虚拟机后,都会在方法区中创建出对应的运行时常量池。JVM为每个类都维护一个常量池
6 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存。它是一种堆外内存
在JDK1.4中引用了NIO(New Input/Output)类,引入了一种基于通道和缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样可以提高应用性能,避免在Java堆和Native堆中来回复制数据。