• JVM的组成


    Java Virtual Machine
    虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现,java虚拟机有自己完善的硬件架构,如处理器,堆栈,寄存器等,还具有相应的指令系统

    一次编译,到处运行

    Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行

    开篇

    程序在执行之前,先要把Java代码编译成字节码(.class文件),JVM首先需要把字节码通过类加载器把文件加载到内存中的运行时数据区中。因为字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要执行引擎将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用本地库接口来实现整个程序的功能。
    我们通常所说的JVM组成指的是运行时数据区,因为通常需要程序员调试分析的区域就是运行时数据区,或者更具体地说是运行时数据区里面的堆(Heap)模块。

    完整构成


    基本构成


    类加载器-ClassLoader

    ClassLoader 负责加载字节码文件即 class 文件
    class 文件在文件开头有特定的文件标示

    魔数:0X CA FE BA BE()
    如果一个文件不是以0xCAFEBABE 开头的,那它肯定不是java class文件

    ClassLoader 只负责class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。

    执行引擎-Execution Engine

    执行引擎,也叫 Interpreter。Class 文件被加载后,会把指令和数据信息放入内存中,Execution Engine 则负责把这些命令解释给操作系统,即将 JVM 指令集翻译为操作系统指令集。

    本地接口-Native Interface

    本地接口的作用是融合不同的编程语言为Java 所用,初衷是融合C/C++ 程序
    它的作用是调用不同语言的接口给 JAVA 用(现在主要是一些硬件交互),它会在 Native Method Stack 中记录对应的本地方法,然后调用该方法时就通过 Execution Engine 加载对应的本地 lib(本地方法库-native libraies)
    原本多用于一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。

    运行时数据区-Runtime Data Area

    Runtime Data Area 是存放数据的,分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)
    几乎所有的关于 Java 内存方面的问题,都是集中在这块


    线程私有的内存区域

    程序计数器(Program Counter Register)

    它是一块较小的内存空间(可能位于cpu的寄存器,有待确认),可以看做是当前字节码指令执行的行号指示器,记录了当前正在执行的虚拟机字节码指令地址,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
    虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,比如分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
    由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是说在任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。
    每个线程都有各自独立的程序计数器,注意如果正在执行的是 Native方法,则程序计数器为空(Undifined),并且 JVM 规范中并没有对程序计数器定义 OutOfMemoryError 异常。

    虚拟机栈(VM Stack)

    虚拟机栈也是线程私有的,它描述的是Java方法执行的内存模型每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
    虚拟机栈帧中,局部变量表是比较为人所熟知的,也就是平常所说的“栈”,局部变量表所需的内存空间在编译期间分配完成,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
    生命周期与线程相同,不存在垃圾回收,线程结束,内存释放
    虚拟机栈有两种异常情况:

    1. StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度,特别是方法的递归调用时(有空程序模拟一下)
    2. OutOfMemoryError:虚拟机栈无法满足线程所申请的空间需求,即使经过动态扩展仍然无法满足时抛出(有空程序模拟一下)

    在这里插入图片描述

    本地方法栈(Native Method Stack)

    本地方法栈与虚拟机栈相似,区别是虚拟机栈为执行 Java 方法服务,而本地方法栈则为 Native 方法服务,有些虚拟机将这两个区域合二为一。

    • 在Execution Engine执行引擎执行时,通过Native Interface 本地接口调用已登记的native library
    • 本地方法栈中抛出异常的情况与虚拟机栈相同,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常

    使用场景:与一些底层系统如操作系统或某些硬件交换信息,如打印机

    线程共享的内存区域

    堆(Heap)

    对于java应用程序来讲,堆是jvm所管理的内存中最大的一块。是被所有线程共享的一块区域,并在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,java程序里“几乎”所有的对象实例都会在这里创建并分配内存。

    • 所有的对象实例以及数组都应当在堆上分配

    堆也是垃圾收集器所管理的主要区域,因此很多时候也被称作GC堆

    从内存回收的角度来看

    由于现在收集器基本都采用分代收集算法,因此堆还可以被细分为:新生代和老年代
    再继续细分可以分为:Eden空间、From Survivor空间(s0)、To Survivor空间(s1)等

    从内存分配的角度来看

    线程共享的堆中还可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
    堆可以是物理上不连续的空间,只要逻辑上是连续的即可,-Xmx和-Xms参数可以控制堆的最大和最小值

    • 堆的空间大小不满足时将抛出OutOfMemoryError异常
    方法区(Method Area)

    用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。
    Java虚拟机规范将方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来。
    误区:方法区不等于永久代

    很多人原因把方法区称作“永久代”(Permanent Generation),本质上两者并不等价,只是HotSpot虚拟机垃圾回收器团队把GC分代收集扩展到了方法区,或者说是用来永久代来实现方法区而已,这样能省去专门为方法区编写内存管理的代码,但是在Jdk8也移除了“永久代”,使用Native Memory来实现方法区

    在方法区中有一部分区域用来存储编译期产生的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。这里需要说明一点,常量并不是只能在编译期产生,运行期间也会产生新的常量并被发在常量池中,如 String 类的 intern() 方法。
    方法区同样会抛出OutOfMemoryError异常

    直接内存

    注意直接内存不属于虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域,其主要用于 JDK1.4 引入的基于通道(Channel)和缓冲区(Buffer)的 NIO 类,可以避免在 Native 堆和 Java 堆之间来回复制数据从而提高性能。
    该部分内存分配不受 Java 堆内存大小的限制,但是肯定也受限于机器硬件内存的限制,在设置虚拟机参数的时候,不能忽略直接内存,把实际内存设置为-Xmx,使得内存区域的总和大于物理内存的限制,从而导致动态扩展时出现OutOfMemoryError异常。

    栈帧构成(Stack Frame)

    一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现

    每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息,方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
    在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中了,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。
    当前栈帧
    一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。
    对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是最有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法。
    执行引擎运行的所有的字节码指令都只针对当前栈帧进行操作

    局部变量表(Local Variable Table

    局部变量表(Local Variable Table)是一组变量值存贮空间,用于存放方法参数和方法内定义的局部变量
    在Java程序编译为Class文件时候,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量
    局部变量表的容量以变量槽为最小单位

    变量槽(Variable Slot)

    每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference和returnAddress
    对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写
    为了节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超过了某个变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用。
    这种机制有时候会影响垃圾回收行为(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存)
    如果执行的是实例方法,那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中可以通过关键字 this 来访问到这个隐含的参数)
    jvm不会给局部变量赋初始值,只给全局变量赋初始值。

    操作数栈(Operand Stack)

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

    当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的

    jvm对操作数栈的优化

    在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递
    操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)

    动态链接(Dynamic Linking)

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

    返回地址(Return Address)

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

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

    当方法返回时,可能进行3个操作

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

    附加信息(Additional information)

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

    帧数据区(Frame Data)

    帧数据区的大小依赖于 JVM 的具体实现
    这部分的作用主要有三部分:

    • 常量池中数据的解析
    • 方法运行完后处理方法返回,恢复调用方现场
    • 方法运行过程中抛出异常时的异常处理,存储有一个异常表,当出现异常时虚拟机查找相应的异常表看是否有相应的Catch语句,假设没有就抛出异常终止这种方法调

    参考

    https://java.jverson.com/jvm/jvm-components.html
    https://baijiahao.baidu.com/s?id=1720844278862268813&wfr=spider&for=pc
    https://blog.csdn.net/wendyyanan/article/details/104261162
    https://www.cnblogs.com/jhxxb/p/11001238.html
    https://juejin.cn/post/6844903655796113421

  • 相关阅读:
    怎样做音乐相册?这篇文章教会你
    Android Jetpack组件架构 :LiveData的使用和原理
    通过AOP拦截Spring Boot日志并将其存入数据库
    力扣动态规划--数组中找几个数的思路
    Git学习使用笔记--(一)
    .NET 7 预览版2 中的 ASP.NET Core 更新
    Set 集合和其之类 HashSet、LinkedHashSet
    云服务器玲琅满目的时代,为什么我独爱Amazon EC2 云服务器?
    JAVA第二课堂选课系统计算机毕业设计Mybatis+系统+数据库+调试部署
    Google codelab WebGPU入门教程源码<5> - 使用Storage类型对象给着色器传数据(源码)
  • 原文地址:https://blog.csdn.net/qq_37380048/article/details/127346825