Java虚拟机之运行时数据区域


他的网站-本文

写在前面

本文作为阅读了周志明作者的 <<深入理解Java虚拟机>> 的读书笔记,同时,也结合了 SE 8 的 JAVA 虚拟机规范。我仅仅在阅读了一章以后,就觉得这是一本非常不错的书(不然也不会有那么多的朋友推荐了),值得入手!


Java 虚拟机定义了在程序执行使用的各种运行时数据区域。其中一些区域是在 Java 虚拟机启动时创建的,只有在 Java 虚拟机退出时才被销毁。其它数据区域是线程私有的。线程私有的数据区域在线程创建时创建,在线程退出时销毁。
运行时数据区域


程序计数器

Java 虚拟机可以同时支持多个线程执行。每个 Java 虚拟机线程都有自己的程序计数器。在任何时候,每个 Java 虚拟机线程都在执行单个方法的代码,即该线程的当前方法。如果该方法不是本地的,则 p c 寄存器(程序计数器)包含当前正在执行的 Java 虚拟机指令的地址。如果线程当前执行的方法是本地的,则 p c 寄存器的值是未定义的。

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

各条线程之间的计数器互不影响,独立存储,这类内存区域为 “线程私有” 的内存。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。


Java 虚拟机栈

每个 Java 虚拟机线程都有一个私有的 Java 虚拟机栈, 与线程同时创建。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型,它所需的内存空间在编译期间完成分配。当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

除了push和pop帧外,Java虚拟机堆栈从来没有被直接操作过,所以可以对帧进行堆分配。Java虚拟机堆栈的内存不需要是连续的。

异常状况:

  1. 如果线程中的计算需要比允许的 Java 虚拟机栈更大,则 Java 虚拟机将抛出一个 StackOverflowError

    <深入理解 Java 虚拟机> 一书中描述为线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。

  2. 如果 Java 虚拟机栈可以动态地扩展和拓张,但由于内存不足影响了拓张,或者内存不足以为一个新线程初始化 Java 虚拟机栈,Java 虚拟机抛出一个 OutOfMemoryError

    <深入理解 Java 虚拟机> 一书中提到大部分的 Java 虚拟机都可动态拓展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈。


栈帧

栈帧是用于存储数据和部分结果,以及执行动态链接、方法的返回值和分派异常

每次调用方法时,都会产生一个新的栈帧。当一个栈帧的方法调用完成时,它将被销毁,无论这个完成是正常的还是突然的(抛出一个未捕获的异常)。

栈帧是从创建帧的线程的 Java 虚拟机栈中分配的。每一帧都有自己的局部变量,操作数栈,以及对当前方法类的运行时常量池的引用。局部变量和操作数栈的大小是在编译时确定的。帧数据结构的大小取决于 Java 虚拟机的实现,这些结构的内存可以在方法调用时同时分配。

局部变量

每一个帧都包含一个变量数组,这些变量被称为局部变量。帧的局部变量数组的长度是在编译时确定的,并在类或接口的二进制表示中提供,以及帧相关的方法的代码。

一个局部变量可以保持 boolean、byte、char、short、int、float、reference 或 returnAddress 类型的值,一对局部变量可以保存 long 或 double 类型的值。

Java 虚拟机使用局部变量在方法调用时传递参数。在类方法调用中,所有参数都从局部变量0开始的连续局部变量的形式传递。在实例方法调用上,始终使用局部变量0向调用实例方法的对象传递引用(也就是 Java 编程语言中的 this)。随后,所有的参数都从局部变量 1 开始的连续局部变量中传递。

操作数栈

每一个帧都包含一个后进先出的栈,称为操作数栈。一个栈帧的操作数栈的最大深度是在编译时确定的,并与该帧相关的方法的代码一起提供。

当创建包含操作数栈的帧时,操作数栈为空。Java 虚拟机提供指令将常量或者值从本地变量或字段加载到操作数栈中。其它虚拟机指令从操作数栈中获取操作数,对它们进行操作,然后将结果推回操作数堆栈。

操作数堆栈还用于准备要传递给方法的参数和接收方法的结果。

操作数堆栈操作的限制是通过类文件验证来强制执行的。在任何时间点,操作数栈都有一个关联的深度,其中 long 或 double 类型的值为深度贡献两个单位,任何其他类型的值为深度贡献一个单位。

动态连接

每一栈帧都包含对当前方法类的运行时常量池的引用,以支持方法代码的动态连接。

方法的类文件代码通过符号引用引用要调用的方法和要访问的变量。动态连接将这些符号方法引用转换为具体的方法引用,根据需要加载类来解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关联的存储结构中的适当偏移量。这种方法和变量的后期绑定使得方法使用的其他类中的更改不太可能破坏此代码。

方法调用正常完成

如果调用没有引发异常(直接从Java虚拟机抛出或者由于显示的抛出语句而抛出),那么方法调用将正常完成。

如果对当前方法的调用正常完成,则可能会向调用方法返回一个值。当被调用的方法执行返回指令时,返回指令的选择必须与返回值的类型相匹配(如果有的话)。

当前的帧在这种情况下被用来恢复调用程序的状态,包括它的局部变量和操作数堆栈,调用程序的计数器被适当地增加以跳过方法调用指令。然后在调用方法的帧中继续正常执行,返回值(如果有的话)被压入该帧的操作数堆栈。

方法调用突然完成

如果在方法中执行Java虚拟机指令导致Java虚拟机抛出异常,并且该异常没有在方法中处理,那么方法调用突然结束。athrow 指令的执行还会导致显式抛出异常,如果当前方法没有捕捉到异常,则会导致方法调用突然完成。突然完成的方法调用永远不会返回值给它的调用者。

Java 堆

在所有 Java 虚拟机线程之间共享的堆。堆是为所有类实例和数组分配内存的运行时数据区域。

堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统 (称为垃圾收集器) 回收; 对象从不显式释放。Java虚拟机假设没有特定类型的自动存储管理系统,可以根据实现者的系统需求选择存储管理技术。堆的大小可以是固定的,也可以根据计算的需要进行扩展,如果不需要更大的堆,则可以收缩。堆的内存不需要是连续的。

Java虚拟机实现可以为程序员或用户提供对堆初始大小的控制,如果可以动态扩展或收缩堆,还可以控制堆的最大和最小大小。

下列异常情况与堆有关:

  • 如果堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出 OutOfMemoryError

    文档中描述为:“如果计算需要的堆比自动存储管理系统提供的堆多,则 Java 虚拟机抛出 OutOfMemoryError” 。


方法区

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于常规语言的已编译代码的存储区域,或类似于操作系统进程中的 “文本” 段。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法。

方法区域是在虚拟机启动时创建的。虽然方法区域在逻辑上是堆的一部分,但简单的实现可能选择不进行垃圾收集或压缩。此规范并不强制要求方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,也可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。

Java虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,在可变大小方法区域的情况下,还可以提供对最大和最小方法区域大小的控制。

可能在方法区中出现的异常:

  • 如果方法区域中的内存不能满足分配请求,则 Java 虚拟机抛出 OutOfMemoryError

运行时常量池

常量池是方法区的一部分。类或接口的运行时常量池是在 Java 虚拟机创建类或接口时构造的。常量池存放了编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

下列异常情况与类或接口的运行时常量池的构造有关:

  • 在创建类或接口时,如果构建运行时常量池所需的内存超过了 Java 虚拟机的方法区域所能提供的内存,则Java虚拟机抛出OutOfMemoryError

官方文档在 Loading, Linking, Initializing 一章节中有更详细的描述。


本地方法栈

本地方法指 Java 编程语言以外的语言编写的方法。

Java虚拟机的实现可以使用传统堆栈(俗称“C堆栈”)来支持本地方法)。本地方法栈的实现也可以使用一个翻译为Java虚拟机的指令集的语言如c.Java虚拟机实现。无法加载本地方法,自己不依赖传统的本地方法栈栈不需要供应。如果提供,通常在创建每个线程时为每个线程分配本机方法堆栈。

本地方法栈与虚拟机栈所发挥的作用是非常相似,区别在于虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。与虚拟机栈一样,本地方法栈也会抛出 StackOverflowErrorOutOfMemoryError


直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用,而且也可能导致 OutOfMemoryError

书中关于这一部分描述的非常好:

JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是肯定会受到本机总内存的大小及处理器寻址空间的限制。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值