「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
前言
JVM是什么?学了JVM有什么好处?以及运行时数据区是什么?在本篇中将会一个一个的为你解答!
1、为什么要学JVM?
下面的场景你经历过哪些?
- 程序有莫名的卡顿找不到原因?
- 程序运行过程中突然出现OOM现象!
- 每次面试之前先找一堆资料背了JVM相关问题但是,面试过程中问的问题与背的内容存在偏差
- 写出来的代码质量也并不高
2、JVM是一种规范
- JVM到底是什么?
- 所谓的跨语言性是什么?
- 为什么说JVM是一种规范
JVM到底是什么?这就要从对应的Java程序的执行过程开始说起了!
2.1 Java程序的执行过程
一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到方法区,执行 引擎将会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译 存在,输入字节码,调用操作系统函数。 过程如下:Java 文件->编译器>字节码->JVM->机器码。 JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀 的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作
这里我们提到:字节码过后就是JVM!那么来看看对应关系如何?
2.2 字节码文件与JVM
我们平时说的JAVA字节码,指的是JAVA语言编译(通过javac编译.java后缀文件)成的字节码,准 确的说任何能在JVM平台上执行的字节码格式都是一样的,所以应该统称为JVM字节码。
不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的JVM上运行。【因此,这就是所谓的跨语言性】
注意:JAVA虚拟机与JAVA语言并没有直接联系,他只是特定的二进制文件格式.class文件有所关联,CLASS文件中包含JVM虚拟机指令集(bytecodes)和符号表,还有一些其他辅助信息。
2.2.1 JVM跨语言性的设计思路
如图所示
Java之所以是跨语言是因为,不同的语言通过编译器都统一编译成JVM可识别的字节码文件,JVM只需要识别对应的字节码即可!
JAVA编译器指令流是基于栈的指令集架构,而另一种指令集架构为基于寄存器的指令集架构!
那么!何为栈的指令集架构? 何为寄存器的指令集架构?
2.3 栈指令集架构与寄存器指令集架构
基于栈指令集架构特点:
- 设计与实现简单,适合资源受限的系统;
- 避开寄存器的分配问题:使用0地址指令方式;
- 指令流中的指令操作过程基于栈,且位数小(8位),编译器容易实现;
- 不需要硬件支持,可移植性好!
基于寄存器指令集架构特点:
- X86二进制指令集(区别栈的8位,这里是16位)
- Android 中的Davlik使用的是这种架构
- 依赖于硬件,可移植性插
- 性能优秀和执行更高效
- 花费更少的时间执行一个操作
它们之间的区别:基于寄存器的架构指令往往都是以1~3地址为主,而基于栈则省去地址指令操作,都基于栈区完成
说了那么多,现在来看看如何查看栈指令集
2.3.1 使用 javap命令
在控制台使用命令:javap -v xxx.class
往下滑动控制台,就能看到这段指令集了。
2.3.2 使用 jclasslib 插件
如图所示
这里安装对应插件后,每次修改都需要重新编译,编译成功后,通过这种方式,选择需要查看的class文件,就可通过如下方式查看对应的指令集
如图所示
我们可以看到:栈指令都是一条一行,而寄存器指令集则是组装指令(后面篇章会讲解)
这里说到,Java和Android使用的是不一样的指令集架构,那么它们的虚拟机呢?
2.4 Android虚拟机与JVM的关系
2.4.1 Hotspot虚拟机
HotSpot:
- 隶属:sun
HotSpot历史发展版本:
- 最初由Longview Technologies设计开发
- 97年被Sun公司收购,09年Oracle收购sun
- JDK1.3开发Hotspot成为默认虚拟机
- 现阶段占据JAVA语言虚拟机市场的绝对地位
- 一般面试所有提到的JVM虚拟机都默认指代的是Hotspot虚拟机
2.4.2 Dalvik虚拟机&ART虚拟机
Dalivk:
- 隶属:Google
发展历史:
- 应用于Android系统,并且在Android2.2中提供了JIT,发展迅猛
- Dalvik是一款不是JVM的JVM虚拟机
- 本质上他没有遵循与JVM规范,不能直接运行java Class文件
- 他的结构基于寄存器结构,而不是JVM栈架构
- 执行的是编译后的Dex文件,执行效率较高
- 与Android5.0后被ART替换
注意哟!这里说到不是JVM的JVM虚拟机,虽然他没有遵循JVM规范,但是他并非完全没遵循JVM规范。
综上,JVM是一种规范!
OK,现在知道它俩关系后!来看看JVM内部长啥样!
2.5 JVM内部结构
如图所示
JVM由三大构成组件分别为:类加载器、运行时数据区、执行引擎组成!
而在Android虚拟机里面,对应的类加载器以及执行引擎并非是JVM里的
只是运行时数据区与JVM的相似,因此本篇重点将会详解JVM的运行时数据区!
如图所示
那么在运行时数据区里,到底做了什么呢?
2.5.1 运行时数据区
如图所示
在运行时数据区里:分为堆和栈!(这两个稍后讲解)
值得注意的是,这里的直接内存!
2.5.1.1 直接内存
- 直接内存不是虚拟机运行时数据区的一部分,也不是JAVA虚拟机规范中定义的内存区域
- 这块区域会被频繁使用,在java堆内dictiByteBuffer对象直接引用操作
- 这块内存不收java堆的大小限制,但是受本机总内存的限制,可以通过maxDirectMemorySize来设置
2.5.1.2 堆在内存中的职责
- 堆是运行时存储单位
- 解决数据存储问题,数据往哪放,怎么放!
如图所示
就拿炒菜来说,堆的作用就是:存储这些炒菜所需要的东西!肉装哪个盘子,尖椒放哪个碗里等等!
2.5.1.3 栈在内存中的职责
- 栈是运行时处理单元!
- 栈用来解决程序运行问题,比如程序如何执行,如何处理数据,方法怎么执行
如图所示
就拿炒菜来说,栈的作用就是:油温多少成,什么时候放肉,什么时候放尖椒,调料放多少,什么时候装盘,一系列炒菜的处理过程。
既然知道了,堆和栈的作用!那么来看看方法调用的全过程!!!
3、方法调用全过程
3.1 虚拟机栈基本信息
虚拟机栈是什么?
- 承载方法调用的过程中产生的数据容器,随线程开辟,为线程私有
作用:
- 他主管java方法运行过程中所产生的值变量、运算结果、方法的调用与返回等信息管理主核心:
- 局部变量、计算结果
结构作用:
- 栈结构的应用能产生一种快速有效的分配方案,
- 访问速度仅次于程序计数器
- JAVA直接堆栈操作只有两个:出栈、入栈
- 此种应用不需要有GC设定
这里提到程序计数器,那么程序计数器是什么呢?
3.2 程序计数器
程序计数器,又名PC寄存器。
如图所示
每一项指令前的自增的下标就是程序计数器,它记录了每一条指令在什么位置!当程序运行时,将会通过计数器执行对应的指令!
因此程序计数器的作用是:明确下一条应该执行什么样的字节码指令
3.3 栈区存储结构与运行原理
如图所示
- 每一个方法表示一个栈帧
- 当调用方法1时,方法1将会作为栈帧压入栈空间,如果方法1调用了方法2,
- 那么方法2将会继续压入栈空间,一直迭代至调用方法5,此时方法5的栈帧在栈顶,方法1在栈底
- 当方法5执行完成时,对应方法5的栈帧将会出栈!
- 以方法5->方法4->方法3->方法2->方法1的顺序依次出栈
总结一句话就是:栈帧遵循的是先进后出的原则!
那如果说方法1调方法2,方法2又调方法1呢?将会发生什么样的故事?
public class StackTest {
public static void main(String[] args) {
StackTest test = new StackTest();
test.work1();
}
public void work1() {
int a = 10;
int b = 20;
work2();
}
public void work2() {
int c = 30;
int d = 40;
work1();
}
}
运行效果
Exception in thread "main" java.lang.StackOverflowError
之所以报这个错,是因为方法不断的向栈空间里面压入栈帧,直到栈空间被撑满了,因此才会报这个错。
上面我们提到过,每一个方法表示一个栈帧,我们看对应栈帧里面,
一个栈帧,除了附加信息,一共有四个结构分别为:局部变量表、操作数栈、方法返回地址、动态链接
3.3.1 局部变量表
- 局部变量表也被称之为局部变量数组或者本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义方法体内的局部变量
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表需要的容量大小是在编译器确定下来的,并保存在方法的code属性的maximum localvariables数据项中,运行期间局部变量表大小不变
- 方法嵌套调用的次数由栈的大小决定,局部变量表决定着栈帧的大小,这里是在编译期就会确定下来
3.3.1.1 局部变量表-jclasslib分析
如图所示
- 每一个方法(除静态方法外),局部变量表里都会有一个this
- slot是局部变量表的基础单位
- 在表中,32位类型数据占用一个slot, 64位数据占用2
- 因此在变量
doubule a
时,对应序号自增了2个单位 - 不满32位的变量也会占用一个slot,比如
byte i
3.3.1.2 slot重复利用问题
刚刚我们看到,在栈帧里,每定义一个变量,对应局部变量表里的序号将会自增对应单位。
那么重复利用是什么情况呢?
如图所示
- 因为b变量作用域仅仅是在方法里的对应代码块里,
- 出了代码块,对应作用域就没了,
- 当定义下一个变量时,将会重复利用
OK,继续下一个!
3.3.2 操作数栈
每一个独立的栈帧中除了包含局部变量表之外还包含一个后进先出的操作数栈
作用:
- 在方法执行过程中根据字节码指令,往栈中写入数据或者提取数据
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入其中
比如:复制、交换、求和、求余等操作
public void test1(){
int i = 10;
int j = 20;
int k = (i + j) * 10;
}
就以这个方法举例,它对应的字节码为:
0 bipush 10
2 istore_1
3 bipush 20
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 bipush 10
11 imul
12 istore_3
13 return
3.3.2.1 执行 0 bipush 10指令
如图所示
当执行对应指令时,对应栈帧(方法)定义对应变量,将会吧对应变量压入操作数栈
3.3.2.2 执行 2 istore_1指令
如图所示
当执行 2 istore_1指令时,先将刚刚压入栈的变量出栈,然后推向局部变量表对应序号 1位置
第二个变量是相同的操作,这里不再演示
3.3.2.3 执行 isload 指令
如图所示
当执行对应iload_x
指令时,将会加载局部变量表对应序号的变量,然后再次压入栈!
3.3.2.4 执行iadd 指令
如图所示
当执行iadd
指令时,先将刚刚加载的数据再次出栈,在栈外执行add方法后,再将结果压入操作数栈里。
后面的指令亦是如此,这里也是按照这样的方式,入栈出栈,最后将结果推向局部变量表对应序号3的位置
OK!操作数栈到这讲解完了,继续下一个!
3.3.3 动态链接
在讲解动态链接之前,需要再次回顾运行区结构
如图所示
在上面说了,堆负责存储栈所需要的数据,此时栈区的动态链接,就需要堆区里面的方法区了!
那么方法区是什么呢?
3.3.3.1 方法区
如图所示
当经过方法区的作用:就是存储类加载器加载的对应方法、属性等,然后给每一个元素加上了#xxx
标识符
这个方法区,它其实就是一个常量池,通过#xxx
标识符 能够访问到对应的元素!
如图所示
- 动态链接就是在指令集里面通过
#xxx
标识符,动态访问方法区已存储的数据。 - 就按这张图来说,动态链接过程:先找到#13,
- 然后#13,找到后面的#92,过后#92还会找它后面所标识的数据
- 整个过程就是动态链接的过程!
OK!继续下一个!
3.3.4 方法返回地址
存放调用方法的PC寄存器的值
一个方法的结束,有两种方式:
- 正常执行完成
- 非正常退出
无论通过那种方式退出,在方法退出后返回到该方法被调用的位置。
不同的是:
- 正常退出是,调用者的PC 寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
- 异常退出,将会通过异常表,跳转至对应异常表位置
这里提到异常表!那么异常表又是什么东西??
3.3.4.1 异常表
如图所示
- 异常表里面存有 起始PC、结束PC、跳转PC,三个属性
- 起始PC、结束PC 这两个包裹的代码内容就是我们常写的
try{xxx}
大括号包裹的内容 - 而跳转PC则是对应异常catch代码行数
异常表就这些内容,很简单吧。
结束语
到这里,本篇内容已经结束了,相信看到这的小伙伴们已经初步了解了JVM基础知识,以及运行时数据区里四大模块是如何工作的!
在下一篇中,将会继续详解运行时数据区中—对象分配过程与完全解析