JVM运行时数据区

1 运行时数据区

java引以为豪的就是内存自动化管理,不需要像C、C++等一样需要开发者手动获取内存、释放内存,对内存进行操作等,java在这方面做的非常好、非常方便。所以,了解java内存区域是怎么划分的是非常有必要的

1.1 结构介绍

在这里插入图片描述

运⾏时数据区也就是JVM在运⾏时产⽣的数据存放的区域,这块区域就是JVM的内存区域,也称为JVM的内存模型——JMM(Java Memory Model)

JMM分成了这么⼏个部分

  • 堆空间(线程共享):存放new出来的对象

  • 元空间(线程共享):存放类元信息、类的模版、常量池、静态部分

  • 线程栈(线程独享):⽅法的栈帧

  • 本地⽅法栈(线程独享):本地⽅法产⽣的数据

  • 程序计数器(线程独享):配合执⾏引擎来执⾏指令

**注:**堆、元空间(含方法区)是线程共享的,线程栈、本地⽅法栈、程序计数器是独享的。

下面每个部分详细介绍一下:

1.1.1 堆空间

Java虚拟机有一个在所有Java虚拟机线程之间共享区域为堆是为所有类实例和数组分配内存的运行时数据区域,它是在虚拟机启动的时候创建的。

Java堆是虚拟机所管理的内存中最大的一块,我们常说的垃圾回收操作的区域就是堆。堆是为所有类实例和数组分配内存的运行时数据区域.

如果是普通对象并且是局部变量,那么在局部变量表(下面会讲述)中存放的只是对象的引用,也就是存储的是对象的地址,实例还是存放在堆区。

堆可以是固定大小的,也可以根据计算的需要进行扩展,如果不需要更大的堆,则可以收缩。堆的内存在物理上不需要是连续的,逻辑上是连续的即可,可通过参

数-Xms(设置堆内存初始值或最小值)和-Xmx(设置堆内存最大值)来对堆内存大小进行扩展。

Java虚拟机实现可以让程序员或用户控制堆的初始大小,如果堆可以动态扩展或收缩,还可以控制最大和最小堆大小。以下异常情况与堆相关联:

  • 如果对象没有足够的内存去分配的话,Java虚拟机会抛出一个OutOfMemoryError。

1.1.2 元空间

方法区是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。

1.1.3 线程栈

线程栈描述的是Java方法执行的线程内存模型,也被称为虚拟机堆栈:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表操作数栈动态连接方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。虚拟机栈是一个栈结构,属于后进先出(FILO)的数据结构。
JVM规范允许Java线程栈具有固定大小或根据计算要求动态扩展和收缩。如果Java线程栈具有固定大小,则每个Java虚拟机堆栈的大小可以在创建堆栈时独立选择。
Java虚拟机实现可以让程序员或用户控制Java虚拟机堆栈的初始大小,以及在动态扩展或收缩Java线程栈的情况下,控制最大和最小大小。
以下异常情况与 Java 线程栈相关:

  • 如果线程中的计算需要比允许的更大的Java线程栈,则Java虚拟机会抛出一个StackOverflowError。
  • 如果Java线程栈可以动态扩展,并且尝试进行扩展,但没有足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始Java线程栈,则Java Virtual机器抛出一个OutOfMemoryError。

线程栈和栈帧及栈帧内部结构图如下:

在这里插入图片描述

1.1.3.1 局部变量表

栈帧包含一个变量数组,称为局部变量表。局部变量表顾名思义就是局部变量的表,局部变量表存放着8大基本类型、对象引用和returnAddress类型。

1.1.3.2 操作数栈

每个栈帧都包含一个后进先出 (LIFO) 栈,称为其操作数栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。操作数栈上的每个条目都可以保存任何Java虚拟机类型的值,操作数栈中的值必须以适合其类型的方式进行操作,例如整数加法的字节码指令iadd。它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。
操作数栈本质上是JVM执行引擎的一个工作区,也就是方法在执行时才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。

1.1.3.3 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,控制下一条指令执行什么。由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响, 独立存储, 我们称这类内存区域为“线程私有”的内存。

因为JVM内部有完整的指令与执行的一套流程,所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native方法),这个方法不是JVM来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址,所以在执行native方法时,JVM 中程序计数器的值为空(Undefined)。
另外程序计数器也是JVM中唯一不会 OOM(OutOfMemory)的内存区域。

1.1.3.4 返回地址

方法调用完成分为两种方式:方法正常调用完成和方法异常调用完成。

方法正常调用完成:如果直接从Java虚拟机或作为执行显式语句的结果,该调用不会导致抛出异常,则该方法调用正常完成。如果当前方法的调用正常完成,则可能会向调用方法返回一个值。这发生在被调用的方法执行返回指令之一时,选择的返回指令必须适合返回值的类型(如果有)。在这种情况下,当前帧栈用于恢复调用者的状态,包括其局部变量和操作数栈,调用者的程序计数器会适当增加以跳过方法调用指令。然后在调用方法的帧中正常继续执行,并将返回值(如果有)推送到该帧栈的操作数栈中。

方法异常调用完成:如果在方法内执行Java虚拟机指令导致Java虚拟机抛出异常,并且该异常不在方法内处理,则方法调用会突然完成。执行athrow指令 也会导致显式抛出异常,如果当前方法未捕获到异常,则会导致方法调用突然完成。突然完成的方法调用永远不会向其调用者返回值。

方法完成的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:

  • 恢复上层方法的局部变量表和操作数栈
  • 把返回值(如果有的话)压入调用者栈帧的操作数栈中
  • 调整PC计数器的值以指向方法调用指令后面的一条指令等

栈的优化技术——栈帧之间数据的共享
在一般的模型中,两个不同的栈帧的内存区域是独立的,但是大部分的 JVM 在实现中会进行一些优化,使得两个栈帧出现一部分重叠(主要体现在方法中有参数传递的情况),让下面栈帧的操作数栈和上面栈帧的部分局部变量重叠在一起,这样做不但节约了一部分空间,更加重要的是在进行方法调用时就可以直接公用一部分数据,无需进行额外的参数复制传递了。

1.2 程序在执行时运行时数据区中的内存变化及解析

在这里插入图片描述

  • 线程栈:执⾏⼀个⽅法就会在线程栈中创建⼀个栈帧。

  • 栈帧包含如下四个内容:

    • 局部变量表:存放⽅法中的局部变量。
  • 操作数栈:⽤来存放⽅法中要操作的数据。

    • 动态链接:存放⽅法名和⽅法内容的映射关系,通过⽅法名找到⽅法内容。方法的内容是存在堆里的。
    • ⽅法出⼝:记录⽅法执⾏完后调⽤此⽅法的位置。

1.3 创建对象时各个部分存储的位置

在这里插入图片描述

类的class文件存放在方法区中,person作为变量,存放在线程栈中,创建出来的对象存放在堆中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猪大侠0.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值