对于 JVM 运行时内存布局,我们需要始终记住一点:我们即将介绍的这 5 块内容都是在 Java 虚拟机规范中定义的规则,这些规则只是描述了各个区域是负责做什么事情、存储什么样的数据、如何处理异常、是否允许线程间共享等,具体不同的虚拟机有不同的实现方式:
下面用一张图片来描述一下Java文件被JVM加载的内存的过程
程序计数器:
Java是多线程的,cpu可以在多个线程中分配执行时间段。当某一个线程被CPU挂起时,需要记录代码已经执行到的位置,方便CPU重新执行时,知道从哪里开始执行。除了这个,我们常用的分支、循环、跳转、异常处理等也需要依赖程序计数器。
它占用的空间比较小,没有OOM触发规则,是线程私有化数据,生命周期跟随线程。
当一个线程执行java方法时,他记录的是虚拟机字节码的地址。如果执行的是native方法,则计数器的值为空(Undefined)。
虚拟机栈
虚拟机栈是线程私有的,与线程的生命周期同步,Java虚拟机对这个区域定义了两种异常规范:
StackOverflowError
:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。 OutOfMemoryError
:当 Java 虚拟机动态扩展到无法申请足够内存时抛出
JVM在每个方法执行的时候,都会在虚拟机栈中创建一个栈帧。
栈帧(Stack Frame)
它是虚拟机栈进行方法调用和方法执行的数据结构,每一个线程在执行某一个方法时,都会为这个方法创建一个栈帧。
一个线程可以包含多个栈帧,每个栈帧包含局部变量表
,操作数栈
,动态链接
,返回地址
等。
局部变量表
:表示的是变量值的储存空间,方法内部创建的变量和传参都保存在局部变量表中。在java编译成class文件的时候,就会在方法的code属性表中的max_locals数据项中,确定该方法需要分配的最大局部变量表的容量。注意系统不会为局部变量赋值初始值。
操作数栈(Operand Stack)
:也被称为操作栈,它是一个后入先出栈,在java编译成class文件的时候,操作数栈的最大深度在编译的时候也写入code属性表中的max_stacks数据项中。栈中的元素可以是任意java数据类型,当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令被压入栈和弹出操作数栈。
动态链接(Dynamic Linking)
:主要是为了支持在方法调用过程的动态链接。在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转换为其所在内存地址的直接引用,而符号引用存在于方法区中。
返回地址
:在方法退出后都需要返回到方法被调用的位置,程序才可以继续执行,而这个返回地址就是帮助方法恢复上一层的执行状态。
本地方法栈
和虚拟机栈基本相同,只不过针对native方法。在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。
堆(heap)
堆是JVM所管理内存中最大的一块,该区域的唯一目的就是存放对象实例,几乎所有的对象都是在堆中分配,所以它也是GC管理的主要区域,它是所有线程的共享区域。按照对象储存时间的不同,堆的内存又可以划分为新生代和老年代,其中新生代又被划分为Eden和Survivor区
方法区
它是JVM规定的一块运行时数据区,该方法主要是存储已经被JVM加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据,该区域和堆一样,是被各个线程共享的区域。
注意:方法区是jvm规范的一块区域,是个概念,永久区是HotSpot在JDK1.7对方法区的实现。但在JDK1.8移除了,用metaspace的实现方式。
用一张图总结: