JVM内存区域

本文来说下JVM内存区域


概述

JVM内存区域包括PC计数器、Java虚拟机栈、本地方法栈、堆、方法区、运行时常量池和直接内存

本文主要介绍各个内存区域的作用和特性,同时分别阐述各个区域发生内存溢出的可能性和异常类型

在这里插入图片描述


JVM内存区域

Java虚拟机执行Java程序的过程中,会把所管理的内存划分为若干不同的数据区域。这些内存区域各有各的用途,以及创建和销毁时间。有的区域随着虚拟机进程的启动而存在,有的区域伴随着用户线程的启动和结束而创建和销毁。

JVM内存区域也称为Java运行时数据区域。其中包括:程序计数器、虚拟机栈、本地方法栈、堆、静态方法区、静态常量池等

在这里插入图片描述

注意:程序计数器、虚拟机栈、本地方法栈属于每个线程私有的;堆和方法区属于线程共享访问的


PC计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码行号指示器

  1. 当前线程所执行的字节码行号指示器
  2. 每个线程都有一个自己的PC计数器。
  3. 线程私有的,生命周期与线程相同,随JVM启动而生,JVM关闭而死。
  4. 线程执行Java方法时,记录其正在执行的虚拟机字节码指令地址
  5. 线程执行Native方法时,计数器记录为空(Undefined)。
  6. 唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况区域。

Java虚拟机栈

线程私有内存空间,它的生命周期和线程相同。线程执行期间,每个方法执行时都会创建一个栈帧(Stack Frame) ,用于存储 局部变量表、操作数栈 、动态链接 、方法出口 等信息。

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法出口

每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈和出栈的全过程


局部变量表

局部变量表是一组变量值的存储空间,用于存储方法参数局部变量。 在 Class 文件的方法表的 Code 属性的 max_locals 指定了该方法所需局部变量表的最大容量

局部变量表在编译期间分配内存空间,可以存放编译期的各种变量类型:

  1. 基本数据类型 :boolean, byte, char, short, int, float, long, double等8种;
  2. 对象引用类型 :reference,指向对象起始地址的引用指针;
  3. 返回地址类型 :returnAddress,返回地址的类型。

变量槽(Variable Slot):变量槽是局部变量表的最小单位,规定大小为32位。对于64位的long和double变量而言,虚拟机会为其分配两个连续的Slot空间。


操作数栈

操作数栈(Operand Stack)也常称为操作栈,是一个后入先出栈。在 Class 文件的 Code 属性的 max_stacks 指定了执行过程中最大的栈深度。Java虚拟机的解释执行引擎被称为基于栈的执行引擎 ,其中所指的栈就是指-操作数栈

  1. 和局部变量表一样,操作数栈也是一个以32字长为单位的数组。
  2. 虚拟机在操作数栈中可存储的数据类型:int、long、float、double、reference和returnType等类型 (对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int)。
  3. 和局部变量表不同的是,它不是通过索引来访问,而是通过标准的栈操作 — 压栈和出栈来访问。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈

begin
iload_0    // push the int in local variable 0 onto the stack
iload_1    // push the int in local variable 1 onto the stack
iadd       // pop two ints, add them, push result
istore_2   // pop int, store into local variable 2
end

在这个字节码序列里,前两个指令 iload_0 和 iload_1 将存储在局部变量表中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量表索引为2的位置。

下图详细表述了这个过程中局部变量表和操作数栈的状态变化(图中没有使用的局部变量表和操作数栈区域以空白表示)。

在这里插入图片描述


动态链接

每个栈帧都包含一个指向运行时常量池中所属的方法引用,持有这个引用是为了支持方法调用过程中的动态链接

Class文件常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用:

  1. 静态解析:一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,
  2. 动态解析:另一部分将在每一次的运行期间转化为直接引用,称为动态链接。

方法返回地址

当一个方法开始执行以后,只有两种方法可以退出当前方法:

  1. 正常返回:当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
  2. 异常返回:当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。

当一个方法返回时,可能依次进行以下3个操作

  1. 恢复上层方法的局部变量表和操作数栈。
  2. 把返回值压入调用者栈帧的操作数栈。
  3. 将PC计数器的值指向下一条方法指令位置。

小结

注意:在Java虚拟机规范中,对这个区域规定了两种异常。
其一:如果当前线程请求的栈深度大于虚拟机栈所允许的深度,将会抛出 StackOverflowError 异常(在虚拟机栈不允许动态扩展的情况下);
其二:如果扩展时无法申请到足够的内存空间,就会抛出 OutOfMemoryError 异常。


本地方法栈

本地方法栈Java虚拟机栈发挥的作用非常相似,主要区别是Java虚拟机栈执行的是Java方法服务,而本地方法栈执行Native方法服务(通常用C编写)

有些虚拟机发行版本(例如Sun HotSpot虚拟机)直接将本地方法栈和Java虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。


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

在Java中,堆被划分成两个不同的区域新生代 (Young Generation)老年代 (Old Generation)新生代 (Young) 又被划分为三个区域:一个Eden区和两个Survivor区 - From Survivor区和To Survivor区

简要归纳:新的对象分配是首先放在年轻代 (Young Generation) 的Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次收集仍然存活的,就会被转移到老年代Old中。

这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收


方法区

方法区和Java堆一样,为多个线程共享,它用于存储类信息、常量、静态常量和即时编译后的代码等数据。


运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法和接口等描述信息外, 还有一类信息是常量池,用于存储编译期间生成的各种字面量和符号引用


直接内存

直接内存不属于虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 Java NIO允许Java程序直接访问直接内存,通常直接内存的速度会优于Java堆内存。因此,对于读写频繁、性能要求高的场景,可以考虑使用直接内存。


参考

  1. 周志明,深入理解Java虚拟机:
  2. JVM高级特性与最佳实践,机械工业出版社

本文小结

本文详细介绍了JVM内存区域,后面会继续对JVM相关知识进行深入介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值