一、java虚拟机内存区域

时间:2024-01-14 16:41:14

内存区域

  java虚拟机在java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。java虚拟机规范将JVM管理的内存分为:程序计数器、本地方法栈、Java虚拟机栈、方法区、Java堆。如下图:

一、java虚拟机内存区域

 1、程序计数器

  一块较小的内存空间,可以看做是当前线程执行字节码文件的行号指示器。字节码解释器通过改变计数器的值选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要依赖程序计数器来完成。

  java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间来完成的,在任何一个确定的时刻,一个处理器(多核处理器来说是一个内核)都只会执行一个线程中的指令。因此,为了线程切换后能恢复到正确的执行的位置,每个线程都需要有一个独立的程序计数器,各线程间的计数器互不影响,独立存储。所以该区域是线程私有的。

  如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法(本地操作系统方法),这个计数器的值为空(Undefined)。该内存区域是唯一一个在java虚拟机规范中没有任何OutOfMemoryError情况的区域。 

2、java虚拟机栈

  与程序计数器一样,该内存区域也是线程私有的。java虚拟机栈描述的是java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧,它是用于支持虚拟机进行方法调用和方法执行的数据结构。对于执行引擎来说,活动的线程中,只有栈顶的栈帧是有效的,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存储局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的Code属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

在Java虚拟机规范中,对这个区域规定了两种异常情况:

1) 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常。

2) 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是*Error异常,而不会得到OutOfMemoryError异常。而在多线程环境下,则会抛出OutOfMemoryError异常。

以下是栈帧中存放的各类信息的详细介绍。

2.1、局部变量表

  局部变量表示一组变量的存储空间,存储的是方法参数和方法内部定义的局部变量。其中存放的类型是编译期可知的各种基本数据类型,对象引用(reference)和returnAddress(指向一条字节码指令的地址)。局部变量表所需的空间在程序编译期间就已经确定,即在java文件编译成class文件的时候就已经确定了最大局部变量表的容量。当程序执行到一个方法的时候,这个方法需要在栈中分配多大的局部变量空间就已经完全确定了,在方法运行期间不会改变局部变量表的大小。

  局部变量表的容量以变量槽(Slot)为单位。在虚拟机规范中没有明确指明一个Slot所占用内存空间(允许其随着处理器、操作系统或虚拟机的不同而发生变化),一个Slot槽可以存放一个32位以内的数据类型,boolean、byte、char、short、int、float、reference和returnAddresss。reference是对象引用类型,returnAddress为字节码指令jsr、jsr_w和wet服务的,它指向一条虚拟机指令的操作码。对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。java语言目前规定的64位的数据类型只有long和double两种,reference类型则可能是32位有可能是64位。

  虚拟机通过索引定位的方式使用局部变量表,索引值从0开始到局部变量表的最大Slot数量。对于32位的数据类型,索引n代表第n个Slot槽,对于64位的数据类型,索引n代表第n和第n+1两个Slot槽。

  在方法执行时,虚拟机是通过局部变量表来完成参数值到参数变量列表传递的。如果是实例方法(非static),局部变量表的第0位索引的Slot默认存储实例对象的引用,在方法中可通过"this"关键字访问这个参数。其余参数则是按照参数列表的顺序排序的,从索引1开始的Slot开始存储,当参数列表分配完毕,再根据方法内部定义的变量顺序和作用域分配其余Slot。

  局部变量表中的Slot是可以重用的。在方法体中定义的变量,其作用域不一定涵盖整个方法体,如果当前程序计数器的值已经超过某个变量的作用于,那么这个变量所对应的Slot就可以交给其他变量来使用了。这样的设计不是为了节省空间,而是在某些情况下Slot的复用会直接影响系统的垃圾回收行为。

2.2、操作数栈

  操作数栈又被称为操作栈,操作数栈的最大深度也是在程序编译期间就确定的了,最大深度会被写入到Code属性的max_stacks数据项之中。32位数据占用的操作数栈容量是1,64位的数据占用的操作数栈容量为2。当一个方法开始执行的时候,它的操作数栈是空的,在方法执行中会有各种字节码指令向操作数栈中写入和提取内容,也就是出栈和入栈的操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。

  java虚拟机的解释执行引擎称为"基于栈的执行引擎",其中所指的栈就是操作数栈,因此我们也称java虚拟机是基于栈的。它和Android的虚拟机执行引擎不同,Android是基于寄存器的。

  基于栈的指令集最大的优点是可移植性强,主要缺点是效率会相对低一些。由于寄存器是硬件直接提供,因此基于寄存器的指令集优点是执行效率高,缺点是可移植性差。

2.3、动态连接

  每个栈帧中都包含一个指向运行时常量池中的该栈帧所属方法的引用,持有这个引用是为了方便方法调用过程中的动态连接。Class文件的常量池中存在大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用为参数,这些符号引用,一部分会在方法加载阶段或者第一次调用的时候转换为直接引用,如final和static域,成为静态解析,另外一部分则会在每次运行期间转换为动直接引用,这部分称为动态连接。

2.4、方法返回地址

  当一个方法被执行后有两种方式退出这个方法:

    1)执行引擎遇到任意一个方法返回的字节码指令。

    2)遇到异常,并且这个异常没有在方法体里得到处理。

  无论采用何种退出方式,在方法退出后,都需要返回方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用于恢复它的上层方法的执行状态。一般来说方法正常退出,调用者的PC计数器的值就可作为返回地址,栈帧中很可能保存的就是这个计数器的值。而方法异常退出时,返回地址要通过异常处理器来确定,这时候栈帧中一般不会保存这部分信息。 

  方法退出的过程实际上等同于将当前栈帧出栈。因此退出时的操作可能有:恢复上层方法的局部变量表和操作数栈,如果有返回值则把他压入调用者栈帧的操作数栈中。跳帧PC计数器的值以指向方法调用的指令的后一条指令。

3、本地方法栈

  该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。

4、java虚拟机堆

  java虚拟机堆是java虚拟机所管理的内存中最大的一块,他是所有线程共享的区域。几乎所有的对象实例和数组都是在该区域进行内存分配的。java虚拟机堆是垃圾回收的主要区域,因此很多时候也称为"GC堆"。

  根据java虚拟机规范,Java虚拟机堆可以使用物理上不连续的内存空间,只要是逻辑上连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。

5、方法区

  方法区也是线程共享的区域,主要用于存放已经被虚拟机加载的类信息、常量(jdk 1.7 后被移出方法区)、静态变量、即使编译器编译后的代码信息等。方法区又被称为"永久代",只有HotSpot才有永久代。JRockit和IBM J9虚拟机中并不存在永久代的概念。java虚拟机规范把方法区描述为Java虚拟机堆的一部分,而且他和Java虚拟机堆一样不需要物理上连续,大小可固定也可扩展。java虚拟机规范允许该区不进行内存回收。该区域内存回收主要针对废弃常量(Jdk 7 后被移出方法区)和无用的类。运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法和接口外,还有一项信息是常量池(Class文件常量池),用于存放编译器生成的字面量和符号引用,这部分内容将会在虚拟机加载后放入到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另一个特点是具备动态性,java语言并不要求常量一定是在编译期间生成,也就是并非预置入Class常量池的内容才能进入方法区的运行时常量池,运行期间也可将新的常量加入到运行时常量池中,这种特性运用比较多的是String的intern()方法。

  根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

6、直接内存

  直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。