Java内存区域

运行时数据区域

运行时数据区域

程序计数器

字节码解释器通过更改这个计数器的值来选取下一条需要执行的字节码指令。每条线程都需要一个独立的程序计数器, 线程之间, 互不影响。Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域

Java虚拟机栈

线程私有, 生命周期与线程相同。局部变量表存放编译器可知的基本数据类型, 对象引用, 和returnAddress类型。

局部变量表所需要的内存空间在编译期间完成分配, 当进入一个方法时, 在方法运行期间不会改变局部变量表的大小。

这个区域有两种异常:

  1. 当线程请求的栈深度大于虚拟机所允许的深度, 将抛出Stackoverflow异常。
  2. 当虚拟机可以动态扩展, 但无法申请到足够的内存, 将抛出OutOfMemoryError异常。

局部变量表

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

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

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

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

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

操作数栈

操作数栈又常被称为操作栈, 操作数栈的最大深度也是在编译的时候就确定了。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。当一个方法开始执行时, 它的操作栈是空的, 在方法的执行过程中, 会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容, 也就是入栈和出栈操作。

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

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

动态连接

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

方法返回地址

当一个方法被执行后, 有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常, 并且该异常没有在方法体内得到处理。无论采用何种退出方式, 在方法退出之后, 都需要返回到方法被调用的位置, 程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息, 用来帮助恢复它的上层方法的执行状态。一般来说, 方法正常退出时, 调用者的 PC 计数器的值就可以作为返回地址, 栈帧中很可能保存了这个计数器值, 而方法异常退出时, 返回地址是要通过异常处理器来确定的, 栈帧中一般不会保存这部分信息。

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

本地方法栈

和Java虚拟机栈类似, 不过Java虚拟机栈为字节码服务, 而本地方法栈为虚拟机使用的Native方法服务。
这个区域也有可能抛出Stackoverflow异常和OutOfMemoryError异常。

Java堆

Java堆是Java虚拟机管理内存中最大的一块, 被所有线程共享, 在虚拟机启动时创建, 用于存储对象实例。

Java堆是垃圾回收的主要区域。从内存回收角度, Java堆可以细分为:新生代和老年代;新生代再细致一点分为:Eden空间, From Survivor空间, To Survivor空间。从内存分配角度, Java堆可能划分出多个线程私有的分配缓冲区(TLAB)。

可以通过 jvm 选项设定堆容量:

  1. -Xms20M

表示设置堆容量的最小值为 20M, 必须以 M 为单位

  1. -Xmx20M

表示设置堆容量的最大值为 20M, 必须以 M 为单位

方法区

线程共享, 用于存储已被虚拟机加载的类信息, 常量, 静态变量, 编译后的代码等。有可能抛出OutOfMemoryError异常。

运行时常量池

方法区的一部分, 用于存放编译器生成的Class文件中各种字面量和符号引用, 运行期间也可能将新的常量放入池中。

直接内存

直接内存并不是虚拟机运行时区域的一部分, 也不是Java虚拟机规范定义的内存区域。JDK 1.4中新加入的NIO类, 引入了一种基于通道和缓冲区的I/O方式, 它可以使用native函数库直接分配堆外内存, 然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样做是为了能在一些场景中显著提高性能, 因为避免了Java堆和native堆来回复制数据。

本机直接内存的分配不受到Java堆的大小限制, 但会受到物理内存和操作系统的限制。

对象

对象的创建

对内存分配情况分析最常见的示例便是对象实例化:

1
Object obj = new Object();

obj 会作为引用类型(reference)的数据保存在 Java 栈的本地变量表中, 而在 Java 堆中保存该引用的实例化对象, Java 堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等), 这些类型数据则保存在方法区中。

虚拟机遇到一条new指令时, 首先将去检查这个指令的参数是否能在常氮池中定位到一 个类的符号引用, 并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有, 那必须先执行相应的类加载过程。

在类加载检查通过后, 接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定, 为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

如果Java堆中内存是绝对规整的, 那么分配内存就只需要把指针移动一个对象大小的距离, 这种分配方式叫指针碰撞;如果已使用的内存和空闲内存相互交错, 虚拟机就需要维护一个空闲内存的列表, 在分配的时候从列表中找到一个足够大的内存分配给对象, 并更新列表, 这种分配方式叫空闲列表