跳至主要內容

运行时数据区详解(内存模型)

Yaien Blog原创大约 6 分钟JavaJVM

运行时数据区详解(内存模型)

JVM运行时数据区数Java虚拟机在运行时对该Java进程占用的内存进行的一种逻辑上的划分,其中包含:方法区、堆内存、虚拟机栈、本地方法栈、程序计数器

方法区(Method Area)

方法区是线程间共享的区域,在JVM启动时创建,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

方法区可以被实现成大小固定或可动态扩展和收缩,如果内存空间不满足内存分配要求就会抛出OutOfMemoryError异常。

对于HotSpot虚拟机而言,在JDK 1.8以前,方法区被实现为 “永久代”(Permanent Generation),属于堆的逻辑组成部分,并提供了两个参数调节其大小,-XX:PermSize用于设定初始容量,-XX:MaxPermSize用于设定最大容量。JDK 1.8之后,HotSpot不再有“永久代”的概念,类的元信息数据迁移到被称为“元空间”(Metaspace)的新区域,而静态变量、常量等则存储于堆中。元空间没有使用堆内存,而是分配在 本地内存(直接内存) 中,默认情况下其容量只受可用的本地内存大小限制。类似地,HotShot虚拟机也提供了两个参数来调节其大小, -XX:MetaspaceSize用于设定初始容量,-XX:MaxMetaspaceSize用于设定最大容量

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量符号引用,这部分内容将在类加载之后进入到方法的运行常量池中存放。

字面值常量:

  • 字符串字面量
  • 用final修饰的基础类型成员变量的字面值
  • 由字面值常量相加得到的结果

直接内存(Direct Memory)

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,因此直接内存的分配不受Java堆大小的限制,但是还是会受到本机总内存(RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。

直接内存主要用于NIO类库,实现基于通道(Channel)和缓冲区(Buffer)的IO方式。通常,当需要处理大量数据的读写操作时,可以考虑使用直接内存,例如文件传输、网络编程等。

直接内存优点在于可以避免在Java堆和本地堆之间复制数据,提高IO操作的性能,缺点就是分配和释放代价较高,而且不受JVM垃圾回收的管理,容易造成内存溢出。

直接内存可以通过ByteBuffer类的allocateDirect方法来分配和操作,

堆内存(Heap)

堆内存是Java虚拟机中内存最大的一块,也是被所有线程共享的,在虚拟机启动时创建,Java对唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配。

虚拟机栈(Virtual Machine Stack)

虚拟机栈(线程栈)描述的是Java方法执行的内存模型,是线程私有的,它的生命周期与线程相同。当每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

栈帧在线程栈中属于先进后出(FILO),每个方法从调用知道执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表(LVT)

局部变量表是一个索引以0开始的字节数组,存储了一个方法的所有入参和局部变量。LVT所存储的类型都是编译期可知的,包括各基础类型(byte、char、short、int、long、float、double、boolean)、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)

LVT有如下特点:

  • 第0个Slot(槽位)固定存储指向方法所属对象的this指针
  • 除了long和double占用连续两个Slot之外,其余类型只占用一个Slot
  • LVT按照变量的声明顺序进行存储

操作数栈(OS)

操作数栈用于在方法运算过程存储其中间的运算结果、方法入参和返回结果,它是一个后进先出(Last-In-First-Out,LIFO)的队列。

JVM提供了对OS出栈和入栈的指令,如load指令属于入栈指令、store指令属于出栈指令。

动态链接

每个栈帧内都包含一个指向当前方法所属类的运行时常量池引用,也称为符号引用(Symbolic Reference),用于在类加载阶段对代码进行动态链接。动态链接所做的就是根据符号引用所表示名字,转换成对方法或变量的实际引用,从而实现运行时绑定(Late Binding)。

本地方法栈(Native Method Stack)

本地方法栈的作用与Java虚拟机栈类似,区别在于后者是为Java方法服务,而本地方法栈则为native方法服务。Java虚拟机规范没有对native方法机制及其实现语言做强制规定,如果JVM不提供native方法,则无需实现本地方法栈。

本地方法栈既可以被实现成固定大小,也可以实现成可动态地扩展和收缩,因此在特定的场景下也会抛出StackOverflowError异常和OutOfMemoryError异常。

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

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

上次编辑于:
贡献者: yanggl