Java 虚拟机在执行Java程序时,会将它所管理的内存划分为几个不同的内存区域,这些区域的用途不尽相同,有不同的创建和销毁时间。有些区域随着虚拟机线程的启动而创建,有些随着用户线程的启动和结束而创建和销毁。

1、运行时数据区域

JVM 在运行Java程序时,会将它所管理的内存划分为几个不同的内存区域。分别是:
    1.程序计数器
    2.虚拟机栈
    3.本地方法栈
    4.方法区
    5.堆

如下图所示:
img
当然,每一个不同的内存区域都有不同的生命周期;有的是随着JVM的启动和停止而创建和销毁,有的是随着用户线程的启动和结束而创建和销毁。

1.1、程序计数器

    程序计数器是一块比较小的内存区域。它的作用可以看做是线程执行字节码的行号指示器。
在JVM的概念模型里,字节码解释器就是通过修改该内存区域的值来选取下一条需要执行的代码。所以,代码的分支,循环,跳转和线程
恢复等基础功能都依赖于该内存区域。
    在Java的多线程环境里,各线程的执行是通过轮流切换来分配处理器的执行时间。所以在任何一个时间点,一个处理器(多核处理
    的一个内核)只会执行一个线程,这样就会存在线程切换后恢复的问题。所以,每一个线程都需要有一个独立私有的内存区域(线程
    计数器),并且互不影响。此外,程序计数器在执行不同的方法时存储的值是有一定区别的,在执行java方法是存储的是字节码指
    令的地址,在执行 native 方法时,存储的是空值( Undefined )。
  **程序计数器是虚拟机规范中唯一一个没有规定 OutOfMemoryError 的内存区域**

1.2、虚拟机栈

虚拟机栈是当前线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java 方法执行的内存模型:
    每个方法被执行的时候都会同时创建一个栈帧( Stack Frame )用于存储局部变量表、操作栈、动态链接、方法出口等信息。
    栈帧里面存放的是方法的局部变量,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成。每一个方法被调用直至执
    行完成的整个过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    如果方法中调用另一个方法,则会把对应方法的栈帧压栈,此时这个方法的栈帧就在栈顶,执行完该方法就会把该方法的栈帧出栈。
    对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行
    的所有字节码。指令都只针对当前栈帧进行操作。  

虚拟机栈示意图:
img

1.2.1 局部变量表

    局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的
Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。局部变量表的容量以变量槽(Slot)为最小单位,
32位虚拟机中一个Slot可以存放一个32位以内的数据类型( boolean、byte、char、short、int、float、reference 和
returnAddress 八种)。reference 类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接
或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。 returnAddress 类型是为字节码指令 jsr、jsr_w
和 ret 服务的,它指向了一条字节码指令的地址。虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法
(非static ),那么局部变量表的第0位索引的 Slot默认是用于传递方法所属对象实例的引用,在方法中通过 this 访问。Slot
是可以重用的,当 Slot 中的变量超出了作用域,那么下一次分配Slot 的时候,将会覆盖原来的数据。Slot 对对象的引用会影响
GC(要是被引用,将不会被回收)。系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量
那样的准备阶段。
1.2.2 操作栈
    和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准
的栈操作——压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。虚拟机在
操作数栈中存储数据的方式和在局部变量区中是一样的:如 int、long、float、double、reference 和 returnType 的存储。对
于 byte、short 以及 char 类型的值在压入到操作数栈之前,也会被转换为 int。虚拟机把操作数栈作为它的工作区——大多数指令都
要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd 指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压
+回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个 int类型的局部变量相加,再把结果保存到第三个局部变量的:
1
2
3
4
5
6
1. begin  
2. iload_0 // push the int in local variable 0 onto the stack
3. iload_1 //push the int in local variable 1 onto the stack
4. iadd // pop two ints, add them, push result
5. istore_2 // pop int, store into local variable 2
6. end
1.2.3 动态链接
虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,  但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果不是在运行期间转换为直接引用,那么这种转换就成为动态连接.
1.2.4 方法出口(返回地址)
方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,果是因为异常退出的,则是需要通过异常处理表来确定。方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括:恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。
1.2.5 异常
在Java 虚拟机规范中,对虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

1.3 本地方法栈

本地方法栈和虚拟机栈非常相似,只是虚拟机栈视为JVM执行java方法而服务的,本地方法是为JVM执行本地方法( native )方法服务的,JVM规范中并没有对本地方法栈的实现作出强制规定,所以本地方法栈可以不是通过字节码执行引擎操作的,如果以字节码实现的话,虚拟机栈和本地方法栈可以合二为一,实际上,HotSpot虚拟机就是把本地方法栈和虚拟机栈合二为一的。与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutofMemoryError 异常。

1.4 堆

堆是JVM所管理的内存中最大一块,被所有线程共享,在虚拟机启动时创建,唯一的目的就是存放对象实例,几乎所有的对象实例都在该区域分配内存。
虚拟机规范中描述:所有的对象和数组都要在堆上分配内存,但是,由于JIT编译器和逃逸分析技术的发展,栈上替换和标量分配优化技术导致所有对象都在堆上分配内存就不是那么绝对了。堆是垃圾收集器管理的主要区域,所以Java堆也被称之为 GC堆。
JVM规范表示Java堆的内存空间在物理上不一定需要连续,但是在逻辑上需要连续。在实现时,大小可以是固定的,也可以是可扩展的。当前主流的虚拟机都是按照可扩展的方式实现,可以通过-Xms 和 -Xmx 来控制。
如果堆内存不足(堆没有足够的内存分配给对象实例),并且无法扩展时,就会抛出 OutofMemoryError 异常。

1.5 方法区

与Java堆一样,都是线程共享的,主要存储已被虚拟机加载的类信息,常量,静态变量和即时编译器编译后的代码。
JVM规范中堆该区域的限制非常宽松,除了和堆一样不需要连续物理内存以外,还可以不实现垃圾回收,但是,这并不意味着进入方法区的数据就会永久存在,因为这块内存区域有针对常量池和类型卸载的垃圾回收,只是这个垃圾回收的效果不是很令人满意,主要是类型卸载的回收条件相当苛刻。JVM规范中指出,当方法区的内存无法满足分配需求时,将会抛出 OutofMemoryError 异常。

1.6 运行时常量池

方法区的一部分, Class 文件中除了有类的版本、字段、方法、接口等描述信息之外,还有常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。对于运行时常量池,Java虚拟机对 Class 文件的每一部分都有严格的规定。但是对于运行时常量池,Java虚拟机规范没有做任何细节的要求。
运行时常量池相对于 Class 文件常量池的另外一个特征就是具备动态性,Java虚拟机运行期间也可能将新的常量放入池中,例如:String 类的 inter() 方法。
当常量池无法再申请到内存是会跑出 OutofMemoryError 异常。


JVM      jvm 内存模型

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!