【JVM】JVM内存模型与操作系统内存模型(一)

JVM内存模型与操作系统内存模型

Java进程在操作系统内存中的结构

在这里插入图片描述

JVM内存模型

在这里插入图片描述

可以这样理解:JVM内存模型其实就是JVM在启动的时候从操作系统内存中要了一块大内存,然后将这个大内存分成五个区域:方法区、堆区、虚拟机栈、本地方法栈、本地方法栈、程序计数器.其实叫JVM运行时区域更合适。但是要区分JVM内存模型与JMM(Java Memory Model)

InstanceKlass:类的元信息(方法区)
InstanceMirrorKlass:镜像类Class对象(堆区)

四个名词:
class文件:即硬盘上的.class文件
class content:类加载器将硬盘上的.class文件读入内存中的那一块内存区域
Class对象:

Class<?> clazz = Test.class

对象:

Test obj = new Test();

方法区

方法区是虚拟机的一种规范
不同版本虚拟机堆方法区的具体实现
永久代(1.8之前是在堆区)
元空间(1.8之后,在直接内存上)

  • 1.永久代的缺点?
    放在堆上,很难触犯类的卸载机制
    1.1 Class对象没有被使用
    1.2 被三大类加载器加载的类不会被卸载,自定义类加载器才会被卸载
    1.3 释放的内存很少
    1.4 为什么早期没有一开始使用元空间的方式呢?早期是没有成熟的动态字节码技术的,现在cglib、asm技术、热更新技术可能会去创建新的类,会造成永久代的OOM,进而会引发堆区的OOM
  • 2.元空间是如何解决?
    2.1 不放在堆区,放在直接内存
  • 3.元空间内部是如何存储的?元空间存在的问题?以及后面会如何优化
    类加载器加载的类在元空间的存储形式。存在的内存碎片化问题。比如说在内存中存在一块4字节单位的区域和一块3字节单位的区域,此时要分配6字节,但虽然内存空间有7字节,但是因为不是连续的,所以导致没法分配。JVM内部不会存在太多。但是自定义类加载器中这个问题会比较明显,如Tomcat可以自己去实现整理算法以调用JNI的形式
    如果一直向下兼容,问题将会一直存在,无法得到解决,以后可能会出现最低版本的支持

为什么要用元空间区替代永久代呢?

  • 1.内存碎片和垃圾回收问题
    永久代是一个固定大小的内存区域,它存储了类的元数据、常量池等。随着时间的推移,永久代可能会发生内存碎片话,导致垃圾回收(GC)效率低下,甚至可能引发OutOfMemoryError错误。元空间使用的是本地内存,并且可以根据需要动态扩展和收缩,从而减少了内存碎片和GC问题
  • 2.更灵活的内存管理
    由于元空间使用的是本地内存,因此它不受JVM堆大小的限制。这意味着可以更灵活地管理内存使用,可以根据应用程序的需要分配更多的内存给元空间,而不会影响到java堆的大小
  • 3.移除预定义的限制
    由于元空间没有这样的预定义限制,它可以根据实际需求动态调整大小,这使得JVM更加健壮和可扩展。如果不指定的话,知道系统内存被使用完
  • 4.简化的JVM架构
    移除永久代简化了JVM的内存模型。现在,JVM的内存主要由堆(heap)、栈(stack)和本地内存中的元空间组成。这种简化有助于提高JVM的维护性和可理解性
  • 5.更好的兼容性
    随着Java应用程序和类库的不断发展,对元数据的需求也在不断增长,使用元空间可以更好地适应这种增长,因为它不受固定大小的限制
  • 6.减少Full GC的影响
    永久代的垃圾回收是Full GC的一部分,这通常会导致较长的停顿时间。由于元空间使用了不同的垃圾回收策略,可以减少Full GC的频率和影响

C++中Hotspot是如何将Klass对象放到方法区的?
C++有个技术叫做操作符重写,/vm/memory/allocation.hpp,操作符重写:new可以指定这个对象存在哪里

Java虚拟机栈

Java虚拟机栈(Java Virtual machine Stacks)是线程私有的,它的声明周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame(栈帧是方法运行时的基础数据结构))用于存储以下几个部分

  • 1.局部变量表
  • 2.操作数栈
  • 3.动态连接
  • 4.方法出口/返回地址
  • 5.附加信息

经常有人把Java内存区分为堆内存(heap)和栈内存(stack),这种分发比较粗糙,java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象分配关系最密集的内存区域是这两块。

虚拟机栈和线程个数比为1:1
一个虚拟机栈中有多少栈帧?跟方法的调用次数成正比

局部变量表

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象的起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress累心地数据,这8种苏韩剧类型,都可以用32位或更小地物理内存来存放,但这种描述与明确指出"每个Slot占用32位长度地内存空间"是有一些差别地,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。只要保证即使在64位虚拟机中使用了64位的物理内存空间区实现一个Slot,虚拟机仍要使用对齐和补白的方式让Slot在外观上看起来与32位虚拟机中的一致。

既然前面提到了Java虚拟机的数据类型,在此再简单介绍一下他们。一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference(Java虚拟机规范中没有明确规定reference类型的长度,它的长度与实际使用32还是64位u虚拟机有关,如果是64位虚拟机,还与是否开启某些对象指针压缩的优化有关,这里暂且只取32位虚拟机的reference长度)和returnAddress8种类型。前面6中不需要多家解释,可以按照Java语言中对应数据类型的概念区理解它们(仅是这样理解而已,Java语言与Java虚拟机中的基本数据类型是存在本质差别的),而第7种reference类型表示对一个对象实例的引用,虚拟机规范既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少都应当能通过这个引用做到两点:

  • 1.从此引用直接或间接地查找到对象在堆中的数据存放的起始地址索引
  • 2.此引用中直接或间接地查找到对象所属数据类型在方法去中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束
    (并不是所有语言的对象引用都能满足这两点,例如C++语言,默认情况下(不开启RTTI支持的其概况),就之只能满足第一点,而不满足第二点。这也是为何C++中提供Java语言里很常见的反射的根本原因)
    对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间,Java语言中明确的(reference类型则可能是32位也可能是64位)64位的数据类型只有long和double两种。值得一提的是,这里把long和double数据类型分割存储的做法与"long和double的非原子性协定"中把一次long和double数据类型读写分割位两次32位读写的做法有些类似。不过局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。
    虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个,Java虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载器的校验阶段抛出异常。

编译优化:方法内部代码块中的变量是不会写到字节码文件中的

为什么C++提供Java语言里反射的原因?
  • 1.现代语言特性需求:随着变成语言的发展,现代变成语言普遍支持反射机制,因为它可以大大提高程序的灵活性和可扩展性。C++作为一门长期发炸你的语言,也在不断地更细你和增加新特性,以保持其竞争力
  • 2.运行时类型信息(RTTI):C++中的反射机制是通过运行时类型信息实现的,这允许程序在运行时获取对象的类型信息,并进行相应的操作。这是实现多态、动态绑定等高级编程概念的基础
  • 3.框架和库开发:反射机制杜宇框架和库的开发尤为重要,因为它可以使这些框架和库更加通用和强大。例如,它可以使序列化、反序列化、对象关系映射(ORM)等操作更加容易实现
  • 4.增强互操作性:C++与其他支持反射的语言(如Java、C#等)进行交互时,反射机制可以提供更好的互操作性。例如C++/CLI是一种特殊的C++方言,用于与.NET框架交互,其中就包含了反射特性
  • 5.动态编程:虽然C++是一门静态类型语言,但在某些情况下,开发者可能需要动态编程的能力,例如在脚本语言或插件系统中。反射可以提供这种能力
  • 6.社区需求:长期以来,C++社区中一直有呼声要求增加反射机制。随着标准的更新,C++委员会逐渐考虑将这些需求纳入语言标准
  • 7.代码生成和元编程:反射机制可以与模板元编程结合使用,以实现更高级的代码生成技术,这在一些复杂的系统中非常有用

需要注意的是,C++的反射机制与传统上Java中的反射并不完全相同。C++的反射能力相对较弱,通常是通过RTTI和模板元编程等技术部分实现的。而且直至2024,C++标准中并没有完整的反射机制,但有一些提案正在尝试将更完整的反射特性引入C++

操作数栈

在这里插入图片描述

操作数栈(Operand Stack)也常称为操作栈,他是一个后入先出(Last In First Out, LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double.32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为。在方法执行的任何时候,操作数栈的深度都不会超过max_stacks数据项中设定的最大值。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递(A方法的返回值作为B方法的入参这种)。
举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int类型的数值,当执行这个指令的时候,会将这两个int值出栈并相加,然后将相加的结果入栈。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整型数假发,它在执行时,最接近栈顶的两个元素的数据类型必须为int类型,不能出现一个long和一个float使用iadd命令相加的情况。
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以公用一部分数据,无需进行额外的参数复制传递,重叠的过程如图所示。Java虚拟机的解释执行引擎称为"基于栈的执行引擎",其中所指的"栈"就是操作数栈。

动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。我们知道Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接

方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这死后可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可以能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现,在实际开发中,一般会把动态链接、方法返回地址与其他附加信息全部归为一类称为栈帧信息

  • 20
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

coffee_babe

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

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

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

打赏作者

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

抵扣说明:

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

余额充值