运行时数据区

羊小咩2022年10月16日
大约 11 分钟

JVM-运行时数据区

本文内容:

  1. JVM介绍&JVM组成部分
  2. 栈指令集架构与寄存器指令集架构
  3. 三种虚拟机介绍
  4. 运行时数据区(重点)

JVM介绍&JVM组成部分

Java程序的执行过程

一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到方法区,执行 引擎将会执行这些字节码。 执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译 存在,输入字节码,调用操作系统函数。

什么是JVM

  • JVM就是一种规范,对于汇编的语言规范和处理,而上层的高级语言实际上我们就理解为是一堆的字符串

  • JVM 全称 Java Virtual Machine,也就是 Java 虚拟机。它能识别 .class后缀 的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作

Java程序的执行过程

Java 文件->编译器>字节码->JVM->机器码

JVM与字节码文件

  • JAVA字节码,指的是JAVA语言编译(通过javac编译.java后缀文件)成的字节码,准确的说任何能在JVM平台上执行的字节码格式都是一样的,所以应该统称为JVM字节码

  • 不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的JVM上运行。

  • JAVA虚拟机与JAVA语言并没有直接联系,他只是特定的二进制文件格式.class文件有所关联, CLASS文件中包含JVM虚拟机指令集(bytecodes)和符号表,还有一些其他辅助信息。

JVM组成部分

JVM由类加载器运行时数据区执行引擎三个部分组成。

JVM组成部分

  • 类加载器:将编译好的class文件加载到JVM进程中
  • 运行时数据区:存放系统执行过程中产生的数据
  • 执行引擎:用来执行汇编及当前进程内所要完成的一些具体内容

栈指令集架构与寄存器指令集架构

JAVA编译器指令是基于栈的指令集架构,而另一种指令是基于寄存器指令集架构。

栈指令集架构与寄存器指令集架构

  • 设计与实现简单,适用于资源受限的系统
  • 避开寄存器的分配问题,使用0地址指令方式
  • 指令流中的指令操作过程基于栈,且位数小(8位),编译器容易实现
  • 不需要硬件支持,可移植性高

栈指令集架构与寄存器指令集架构

  • x86二进制指令集(16位), android的davlik虚拟机使用的是这种架构
  • 依赖于硬件,可移植性差
  • 性能优秀执行高效
  • 更少时间执行一个操作
  • 基于寄存器架构的指令一般以1-3个地址指令为主

三种虚拟机介绍

Hotspot虚拟机

  • Hotspot虚拟机隶属于sun
  • JDK1.3开发Hotspot成为默认虚拟机
  • 一般面试所有提到的JVM虚拟机都默认指代的是Hotspot虚拟机

Dalvik虚拟机&ART虚拟机

  • Dalvik虚拟机隶属于google
  • Dalvik是一款不是JVM的JVM虚拟机。本质上他没有遵循与JVM规范,不能直接运行java Class文件
  • 他的结构基于寄存器结构,而不是JVM栈架构
  • 执行的是编译后的Dex文件,执行效率较高, 于Android5.0后被ART替换

运行时数据区

JVM整体架构:

运行时数据区

  • 线程独享:虚拟机栈,本地方法栈,程序计数器
  • 线程共享:方法区,堆区

虚拟机栈

  • 虚拟机栈数据结构: 先进后出(FILO)的数据结构,
  • 虚拟机栈的作用: 在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址。
  • 虚拟机栈是基于线程的: 哪怕你只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。
  • 虚拟机栈的大小缺省为1M,可用参数 –Xss调整大小,例如-Xss256k。
  • 栈帧:在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。
  • 栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)

同时,虚拟机栈这个内存也不是无限大,它有大小限制,默认情况下是1M。 如果我们不断的往虚拟机栈中入栈帧,但是就是不出栈的话,那么这个虚拟机栈就会溢出。

栈帧

  1. 局部变量表

顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。

1.1 局部变量表slot(变量槽)

slot是局部变量表的基础单位 在表中,32位类型数据占用一个slot, 64位数据占用2位

1.2 slot复用问题

局部变量表中slot是可以服用的,如果一个局部变量过了其他作用域,那么其作用域之后声明的新的局部变量有可能会复用这个solt,以便节省资源。

  1. 操作数栈

存放java方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。 操作数栈本质上是JVM执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。

  1. 返回地址

正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中>)

无论通过那种方式退出,在方法退出后返回到该方法被调用的位置.

  1. 动态连接
  • 每一个栈帧内部都包含一个执行运行时常量池中该栈帧所述方法的引用。包含这个引用的目的是为 了支持当前方法的代码能够实现动态链接(invokeDynamic指令)
  • 在Java源文件被编译到字节码文件中是,所有的变量和方法引用都作为符号引用保存在class文件的 常量池里。
  • 例如:描述一个方法调用另外一个方法是,就是通过常量池中的执行方法符号引用来标识,那么动态 链接的作用就是为了将这些符号引用转换为调用方法的直接引用

程序计数器

  • 较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。

  • 由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令.

  • 因为JVM是虚拟机,内部有完整的指令与执行的一套流程,所以在运行Java方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如果是遇到本地方法(native方法),这个方法不是JVM来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址.

  • 另外程序计数器也是JVM中唯一不会OOM(OutOfMemory)的内存区域。

本地方法栈

  • 本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的(比如Object.hashcode方法)。

  • 本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。 虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一 。

方法区

方法区(Method Area)是可供各条线程共享的运行时内存区域。它用于存储运行时常量池、已被虚拟机加载的类型信息常量静态变量、及时编译器编译后的代码缓存等 方法区是JVM对内存的“逻辑划分”,在JDK1.7及之前很多开发者都习惯将方法区称为“永久代”,是因为在HotSpot 虚拟机中,设计人员使用了永久代来实现了 JVM 规范的方法区。在JDK1.8及以后使用了元空间来实现方法区。

  • 运行时常量池: 运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。
  • 类型信息: 包含类class\接口interface\枚举enum\注解annotation
  • 方法信息: 执行字节码,本地变量表,操作数栈,动态链接,方法出口,异常表

堆区

  • 堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。

  • 堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的.

  • 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。

  • 那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。 Java 的对象可以分为基本数据类型和普通对象。

  • 对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。

  • 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。 当在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

直接内存(堆外内存)

  • 直接内存不是虚拟机运行时数据区的一部分,也不是JAVA 虚拟机规范中定义的内存区域,如果使用了NIO,这块区域会被频繁使用,在java堆内可以用dictiByteBuffer对象 直接引用操作,
  • 这块内存不受java堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。
  • 其他堆外内存,主要是指使用了Unsafe 或者其他 JNI 手段直接直接申请的内存。
  • 堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡

内存溢出问题

内存溢出概念:无法分配内存,可用内存不足,无法完成内存分配 内存溢出场景:

  • 栈溢出
  • 堆溢出
  • 方法区溢出
  • 本机直接内存溢出

总结

  • JVM运行内存的整体流程:

    JVM在操作系统上启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区中 方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。 同时在方法的执行过程中创建的对象一般情况下都是放在堆中,最后堆中的对象也是需要进行垃圾回收清理