您的位置:首页 > 编程语言 > Java开发

深入理解JVM(2)——内存区域与内存溢出

2017-06-09 15:32 435 查看

内存区域

JVM在执行java程序的过程中会将内存分为几个不同的数据区域。java虚拟机规范将JVM所管理的内存区域分为这样几个运行时数据区。

线程共享内存区

java堆

方法区

线程私有内存区

程序计数器

虚拟机栈

本地方法栈

程序计数器

一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。

当线程在执行一个 Java 方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是 Native 方法(调用本地操作系统方法)时,该计数器的值为空。另外,该内存区域是唯一一个在 Java 虚拟机规范中么有规定任何 OOM(内存溢出:OutOfMemoryError)情况的区域。

Java虚拟机栈

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

如果线程请求的栈深度大于虚拟机允许的深度,则抛出StackOverflowError异常;

如果虚拟机在动态扩展栈的时候无法申请到足够的内存,则抛出OutOfMemoryError;

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

详细说明栈帧中存放的各部分信息的作用和数据结构

1.局部变量表

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

局部变量表以slot为单位,JVM规范中并没有规定一个slot应该占用的内存空间的大小(它会随着处理器、虚拟机、操作系统的变化而不同),一个slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference 和 returnAddresss),reference是对象的引用,returnAddress 是为字节指令服务的,它执行了一条字节码指令的地址。对于64位的数据类型,虚拟机会按照高位在前的原则为其分配两个连续的slot。

2.操作数栈

操作数栈又称操作栈,其最大深度也在编译时确定。32位数据占用的栈空间为1,64位为2。用于存储在方法执行过程中产生的各种中间值。

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

基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。

3.动态链接

每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

4.方法返回址

方法执行之后通常有两种方式退出:一种遇到方法返回的字节码指令,另一种是方法异常。无论哪一种都需要返回方法调用的位置,程序才能继续运行。因而栈帧中需要保留一些信息,以便于方法返回时能够到达上层方法的执行状态,一般正常退出时,调用者的PC计数器值就可以用作方法返回址,栈帧中通常会保存这个值,而异常退出的方法返回址则需要异常处理器来确定,栈帧中没有相应值。

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

本地方法栈

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

Java堆

Java Heap 是 Java 虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap 是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。

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

方法区

方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jvm java 内存溢出 内存