JVM运行时数据区

JVM运行时数据区

  • 概要

JVM运行时数据区, 顾名思义, 就是程序运行期间会使用到的数据区域, 其中有些随着JVM的启动而创建, 随着JVM的退出而销毁。另外一些则是与线程一一对应的, 这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

JVM运行时数据区被分为以下几部分:

  1. PC寄存器;
  2. Java虚拟机栈;
  3. Java堆;
  4. 方法区;
  5. 运行时常量池;
  6. 本地方法栈。

首先要明确, 由于JVM也是由其他语言实现的应用程序, 所以在Java语言角度分配在Java Stack中的数据, 从实现JVM的程序角度则是分配在Heap中。所以不要混淆Stack、Heap和Java(VM)Stack、Java Heap的概念。图1-1为运行时数据区结构图。图1-2为JVM内存模型。

图1-1运行时数据区

图1-2 JVM内存模型

  • PC寄存器

PC寄存器, 也叫程序计数器(Program Counter Register), 一块较小的内存空间, 该空间最少能保存一个returnAddress类型的数据或者一个与平台有关的本地指针的值。

它指示了程序下一条需要执行的字节码指令的地址, 所以字节码指示器可以通过改变寄存器的值来选取下一条指令, 分支、循环、跳转、异常处理、线程恢复等基础功能都依赖于寄存器。

从图1-1可以看出, 每个线程都独享一个PC寄存器, 这是由于JVM采用了时间片轮转的方式来进行线程的切换调度, 在任何一个确定的时刻, 一个处理器只会执行一条线程中的指令。为了线程切换后能够恢复到正确的执行位置, 每条线程都需要有独立的PC寄存器, 各条线程之间寄存器互不影响, 独立储存。在JVM规范第9版中有如下描述:

At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method for that thread. If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. 

大概意思就是在任何时候, 每个Java虚拟机线程执行单个方法的内容, 即该线程的当前方法。如果该方法不是native的, 则PC寄存器就保存Java虚拟机正在执行的字节码指令的地址, 如果是native的, 那PC寄存器的值为undefined。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。

  • Java虚拟机栈

与PC寄存器一样, Java虚拟机栈(Java Virtual Machine Stack, 以下简称JVM Stack)也是每个Java虚拟机线程私有的, 这个栈与线程同时创建, 用于存储栈帧(Frames)。栈帧在JVM Stack中有着举足轻重的作用, 这个会在后续介绍。JVM Stack除了栈帧的出栈和入栈, 不会受到其他因素的影响, 所以栈帧可以在堆(Heap,  这里的Heap不是Java Heap)中分配, 也不需要保证所使用的内存是连续的。

JVM Stack有两种实现方式, 固定大小或者动态扩展和伸缩。如果采用固定大小的设计, 那每一条线程的JVM Stack容量在创建时就被独立的选定。并可以通过JVM实现者提供的接口或参数设置JVM Stack的初始容量。如果采用动态扩展和伸缩的设计, 则可以通过参数等手段调节JVM Stack的最大、最小容量(HotSpot好像就是采用这种方式)。

3.1栈帧

栈帧是用来存储数据和部分过程结果的数据结构, 同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

栈帧随着方法调用而创建, 随着方法结束(无论是正常完成还是异常完成都算作方法结束)而销毁。每个栈帧都有自己的局部变量表(Local Variables)、操作数栈(Operand Stack)和指向当前方法所属的类的运行时常量池的引用。

局部变量表和操作数栈的容量在编译时期确定, 并通过方法的Code属性保存及提供给栈帧使用。因此栈帧容量的大小仅仅取决于Java虚拟机的实现和方法调用时可分配的内存。

在一条线程之中, 只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧(Current Frame), 这个栈帧对应的方法就被称为是当前方法(Current Method), 定义这个方法的类就称作当前类(Current Class)。对局部变量表和操作数栈的各种操作, 通常都指的是对当前栈帧的局部变量表和操作数栈进行的操作。当一个新的方法被调用, 一个新的栈帧也会随之创建, 并随着程序控制权移交到新的方法而成为新的当前栈帧。当方法返回的时候, 当前栈帧会传回此方法的执行结果给前一个栈帧, 在方法返回之后, 当前栈帧随之被丢弃, 前一个栈帧重新成为当前栈帧。需要注意的是, 栈帧是线程本地私有的数据, 不可能在一个栈帧之中引用另一个线程的栈帧。

3.1.1 局部变量表

局部变量表为每个栈帧私有的变量列表, 保存了一组局部变量, 变量表的长度由编译期决定, 并存储于类和接口的二进制表示之中, 通过方法的Code属性保存及提供给栈帧使用(不知道有没有翻译错, 附上JVM规范原文:The length of the local variable array of a frame is determined at compile-time and supplied in the binary representation of a class or interface along with the code for the method associated with the frame.), 并且在运行期间不会改变。

一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据, 两个局部变量可以保存一个类型为long和double的数据。

局部变量表采用索引来进行定位访问(也就是像数组一样),  第一个变量的索引值为零, 最大值为局部变量表的最大容量。long和double类型采用两个局部变量表示, 所以需要占用两个索引值, 假设为n, n+1,  那么其定位的索引为n,  且n没有要求必须是偶数, 或者说long和double类型的值不需要在局部变量数组中进行64位对齐。

JVM使用局部变量表来完成方法调用时的参数传递, 当一个类方法被调用的时候, 它的参数会依次保存在从局部变量0开始依次类推的位置上。当一个实例方法被调用时, 局部变量0总是用于存储调用实例方法的对象的引用(即Java语言中的”this”关键字), 参数则保存在随后的连续局部变量中。

3.1.2 操作数栈

每个栈帧内部都有一个后进先出的操作数栈(Operand Stack)。操作数栈的最大长度由编译期决定, 并存储于类和接口的二进制表示中,  通过方法的Code属性保存及提供给栈帧使用(JVM原文:The maximum depth of the operand stack of a frame is determined at compile-time and is supplied along with the code for the method associated with the frame.)。

在上下文明确, 不会产生误解的前提下, 经常当前栈帧的操作数栈直接简称为操作数栈

操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。JVM提供一些字节码指令来从局部变量表或对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候, 操作数栈也用来准备调用方法的参数以及接收方法返回结果。

在操作数栈中, 数据必须被正确的操作, 即操作数栈的操作必须与操作数栈栈顶的数据类型相匹配, 例如不可以入栈两个int类型的数据, 然后当作long类型去操作他们。

当然也有一部分Java虚拟机指令(如dup和swap指令)可以不关注操作数的具体数据类型, 但这些指令不可以用来修改数据, 也不可以拆散那些原本不可以拆散的数据, 这些操作的正确性将会通过Class文件的校验过程来强制保障。

在任意时刻, 操作数栈都会有一个确定的栈深度, 一个long或者double类型的数据会占两个单位的栈深度, 其他数据类型则会占用一个单位深度。

3.2 动态链接

每个栈帧内部都包含一个指向运行时常量池的引用来支持当前的代码实现动态链接(Dynamic Linking)。在Class文件里面, 描述一个方法调用其他方法, 或者访问其他成员变量是通过符号引用(Symbolic Reference)来表示的, 动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。类加载的过程中将要解析掉尚未被解析的符号引用, 并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量。

3.3 方法正常调用完成

方法正常调用完成是指方法的执行过程中, 没有任何异常被抛出——包括直接从Java虚拟机之中抛出的异常已经在执行时通过throw语句显示抛出的异常。如果当前方法调用正常完成的话, 它很可能会返回一个值给调用它的方法, 方法正常完成发生在一个方法执行过程中遇到了方法返回的字节码指令的时候, 使用哪种返回指令取决于方法返回值的数据类型(如果有返回值的话)。

在这种场景下, 当前栈帧承担着恢复调用者状态的责任, 其状态包括调用者的局部变量表、操作数栈和被正确增加过来表示执行了该方法调用指令的PC寄存器等。使得调用者的代码能在被调用的方法返回并且返回值被推入调用者栈帧的操作数栈后继续正常地执行。

3.4 方法异常调用完成

方法异常调用完成是指在方法的执行过程中, 某些指令导致了Java虚拟机抛出异常, 并且虚拟机抛出的异常在该方法中没有办法处理, 或者在执行过程中遇到了athrow字节码指令显示的抛出异常, 并且该方法内部没有吧异常捕获住。如果方法异常调用完成, 那一定不会有方法返回值返回给它的调用者。

Java虚拟机栈可能发生如下异常情况:

  1. 如果现场请求分配的栈容量超过Java虚拟机栈允许的最大容量时, Java虚拟机将会抛出一个StackOverflowError异常。
  2. 如果Java虚拟机栈可以动态扩展, 并且扩展的动作已经尝试过, 但是目前无法申请到足够的内存去完成扩展, 或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈, 那Java虚拟机将会抛出一个OutOfMemoryError异常。
  • 本地方法栈

本地方法栈(Native Method Stack)很相似, 首先本地方法栈也是线程私有的。其次, 本地方法栈所发挥的作用也和虚拟机栈相似, 区别在与虚拟机栈为虚拟机执行Java方法(也就是字节码), 而本地方法栈则为虚拟机使用到的Native方法服务。有些虚拟机使用其他语言(如C语言)来实现指令集解释器时, 也会使用到本地方法栈。如果Java虚拟机不支持native方法, 并且也不依赖于传统栈, 则无需支持本地方法栈, 如果支持, 则这个栈一般会在线程创建的时候按线程分配。

和Java虚拟机栈一样,本地方法栈也可以实现为固定大小的或根据计算动态扩展和收缩的, 且规则与Java虚拟机栈一样, 所以本地方法栈也会发生StackOverflowError与OutOfMemoryError异常。

  • Java

在Java虚拟机中, Java堆(Java Heap)是可供各条线程共享的运行时内存区域, 也是Java虚拟机所管理的内存中最大的一块。此内存区域的作用就是存放类实例和数组对象, 几乎所有的对象实例都在这里分配内存。在Java虚拟机规范中是所有的对象实例及数组对象都在堆中分配内存, 但随着JIT编译器的发展和逃逸分析技术逐渐成熟, 栈上分配、标量替换优化技术将会导致一些微妙的变化, 所有的对象都分配在堆上也渐渐变得不那么绝对了。

Java堆在虚拟机启动的时候就被创建, 它存储了被自动内存管理系统(Automatic Storage Management System, 也即是常说的Garbage Collector(垃圾收集器))所管理的各种对象, 这些受管理的对象无需, 也无法显示地被销毁。Java堆从内存回收的角度来看, 由于现在收集器基本都是采用分代收集算法, 所以Java堆中还可以细分为: 新生代和老年代(在HotSpot虚拟机中还有永久代, 也就是方法区的内存区域); 再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看, 线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。如此进一步划分的目的仅仅是为了更好的回收内存, 或者更快的分配内存, 与存放内容无关, 这些划分的区域存放的仍然是对象实例。

Java虚拟机规范对于Java堆所使用的内存没有要求是连续的, 只要逻辑上是连续的即可。在实现时, 可以是固定大小的, 也可以随着程序执行的需要动态扩展, 并在不需要过多空间时自动收缩。对于固定大小的实现方式, 因提供设置Java堆初始容量的手段, 对于动态扩展和收缩的Java堆来说, 则应当提供调节其最大、最小容量的手段。当前主流的虚拟机一般都是采用可扩展的方式来实现的, 并且通过-Xmx和-Xms控制。

Java堆可能发生如下异常情况:

如果实际所需要的堆超过了自动内存管理系统能提供的最大容量, 那Java虚拟机将会抛出一个OutOfMemoryError异常。

  • 方法区

Java虚拟机规范描述:在Java虚拟机中, 方法区(Method Area)是可供各条线程共享的运行时内存区域。方法区域传统语言中的编译代码储存区(Storage Area Of Compiled Code)或者操作系统进程的正文段(Text Segment)的作用非常类型, 它存储了每一个类的结构信息, 例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化是用到的特殊方法。

虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却又一个别名叫做Non-Heap(非堆), 目的应该是与Java堆区分开来。

上文中提到, 在HotSpot虚拟机中, Java堆中的永久代是方法区的内存区域, 这是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区, 或者说使用永久代类实现方法区, 这样HotSpot的垃圾收集器可以向管理Java堆一样管理这部分内存, 这样可以节省为方法区专门开发内存管理的开发工作。 也就是说方法区和永久代在本质上是不等价的, 仅仅只是HotSpot的设计团队为了方便才采用了永久代来实现方法区。 而且在其他虚拟机(如BEA JRockit、IBM J9)来说, 并不存在永久代的概念。在JVM虚拟机规范中并没有约束JVM实现者如何方法区, 并且对于简单的JVM实现来说, 可以不实现方法区的垃圾收集。在Java7时, HotSpot设计团队将运行时常量池从永久代移除, 在Java堆中开辟了一块区域存放运行时常量池, 到了Java8时, 设计团队已经将永久代移除, 将方法区从Java堆内存中移到了本地内存(Native Memory)中, 称之为元空间(Metaspace)。图6-1为Java7内存示意图, 6-2为Java8。

图6-1 Java7

图6-2 Java8

将方法区移动Native Memory后, HotSpot提供了一个参数-XX:MaxMetaSpaceSize设置MetaSpace内存的大小。

方法区的容量设置与Java堆一样, 可以是固定大小的, 也可以是动态扩展和伸缩的。在Java8以前, 方法区可能会发生如下异常:

如果方法区的内存空间不能满足内存分配请求, 那么Java虚拟机将会抛出一个OutOfMemoryError:PermGen space异常。而到了Java8 则会抛出一个OutOfMemoryError:Metadata space异常。

  • 运行时常量池

运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式, 它包括了若干种不同的常量: 从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。每个运行时常量池都分配在Java虚拟机的方法区之中, 也就是说运行时常量池是方法区的一部分, 在类和接口被加载到虚拟机后, 对应的运行时常量池就被创建出来。一般来说, 除了保存Class文件中描述的符号引用外, 还会吧翻译出来的直接引用也存储在运行时常量池中。

运行时常量池还有一个重要的特征是具备动态性, Java语言并不要求常量一定只有编译期才能产生, 也就是说并非预置入Class文件中常量池(Constant_Pool)的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入运行时常量池中, 例如比较常见的String类的intern()方法。

Note:在Class文件中, 存在一个Constant_Pool的数据结构, 也被叫做Class文件中的常量池, 在类或接口被加载后, 存放进运行时常量池。

在创建类和接口的运行时常量池时, 可能会发生如下异常情况:

当创建类或接口的时候, 如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值, 那Java虚拟机将会抛出一个OutOfMemoryError异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值