JVM系列:内存区域

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。 其中一些数据区域是在Java虚拟机启动时创建的,仅在Java虚拟机退出时销毁,线程共享。其他的数据域是基于每个线程的,这些线程数据域在线程创建时创建,线程销毁时销毁,线程私有。

avatar

程序计数器

记录正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,这个计数器值则为空。

说明

  • 线程私有,由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
  • 内存较小,唯一一个在JVM中没有规定任何OutOfMemoryError情况的区域
  • 字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都依赖这个计数器来完成。

java虚拟机栈

虚拟机栈描述的是java方法执行的内存模型,虚拟机栈的栈元素是栈帧,每个方法在执行的同时都会创建一个栈帧,用于存放局部变量表,操作数栈,方法出口,动态链接等信息,并且入栈;当这个方法返回时,其栈帧出栈。因此,虚拟机栈中栈帧的入栈顺序就是方法调用顺序。

说明

  • 线程私用
  • 生命周期与线程相同。
  • 如果栈请求的深度大于虚拟机允许的深度,则抛出StackOverflowError异常
  • 如果可以动态扩展时,不能申请到足够的内存时,则抛出OutOfMemoryError。

栈帧

栈帧可以理解为一个方法的运行空间,主要有以下几部分组成:

avatar

局部变量表

局部变量表是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其容量用Slot作为最小单位。

操作数栈

后入先出栈,由字节码指令往栈中存数据和取数据,栈中的任何一个元素都是可以任意的Java数据类型。和局部变量类似,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数中写入和提取内容,也就是出栈/入栈操作。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配 2,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。另外我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

动态链接

在运行时创建的直接引用称为动态链接。即程序在编译成.class文件后会有一系列的引用。这些是静态引用。而在运行时才创建的引用称为动态引用。

方法出口

当一个方法开始之后,只有两种方式可以退出这个方法:

  • 执行引擎遇到任意一个方法返回的字节码指令,也就是所谓的正常完成出口。
  • 在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种方式成为异常完成出口。
  • 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,而通过异常退出的,返回地址是要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。本质上,方法的退出就是当前栈帧出栈的过程。

本地方法栈

和虚拟机栈类似,为本地方法服务

此区域唯一目的就是存放对象实例,几乎所有对象都这里分配内存空间,

说明

  • 线程共享
  • 内存最大
  • GC管理的主要区域。当内存不足时会抛出OutOfMemoryError异常

方法区

用于存储虚拟机已经加载的的类信息,常量,静态变量等数据,也叫永久代,JDK8使用元空间代替,该内存不占用JVM内存。

说明

  • 线程共享
  • 内存不足会抛出OutOfMemoryError异常

运行时常量池

运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

直接内存

直接内存,不是Java虚拟机规范中定义的内存区域,但是这一部分仍然会出现OutOfMemoryError异常。

参考资料

  • 深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)