🙊前言:本文章为瑞_系列专栏之《JVM虚拟机》的概述篇,本篇章主要介绍什么是JVM、JVM功能、JVM的组成以及字节码文件的组成。由于博主是从B站黑马程序员的《JVM虚拟机》学习到的相关知识,所以本系列专栏主要针对该课程进行笔记总结和拓展,文中的部分原理及图解也是来源于黑马提供的资料。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!
1 什么是JVM
JVM 全称是 Java Virtual Machine,中文译名“Java虚拟机”。
JVM 本质上是一个运行在计算机上的程序,它的职责是运行Java字节码文件。
瑞:虚拟机就是将字节码指令解释成机器码,机器码交给计算机运行
1.1 JVM功能
- 解释和运行:对字节码文件中的指令,实时的解释成机器码,让计算机执行
- 内存管理:自动为对象、方法等分配内存。自动的垃圾回收机制,回收不再使用的对象
- 即时编译:对热点代码进行优化,提升执行效率
Java需要实时解释,主要是为了支持跨平台特性
所以 Java语言如果不做任何优化,性能不如C、C++等语言
JVM提供了即时编译(Just-In-Time 简称JIT) 进行性能的优化,最终能达到接近C、C++语言的运行性能,甚至在特定场景下实现超越
1.2 常见的JVM
名称 | 作者 | 支持版本 | 社区活跃度 (github star) |
特性 | 适用场景 |
---|---|---|---|---|---|
HotSpot (Oracle JDK版) |
Oracle | 所有版本 | 高(闭源) | 使用最广泛,稳定可靠,社区活跃 JIT支持 Oracle JDK默认虚拟机 |
默认 |
HotSpot (Open JDK版) |
Oracle | 所有版本 | 中(16.1k) | 同上 开源,Open JDK默认虚拟机 |
默认,对JDK有二次开发需求 |
GraalVM | Oracle | 11, 17,19 企业版支持8 |
高(18.7k) | 多语言支持 高性能、JIT、AOT支持 |
微服务、云原生架构 需要多语言混合编程 |
Dragonwell JDK 龙井 |
Alibaba | 标准版 8,11,17 扩展版11,17 |
低(3.9k) | 基于OpenJDK的增强 高性能、bug修复、安全性提升 JWarmup、ElasticHeap、Wisp特性支持 |
电商、物流、金融领域 对性能要求比较高 |
Eclipse OpenJ9 (原 IBM J9) |
IBM | 8,11,17,19,20 | 低(3.1k) | 高性能、可扩展 JIT、AOT特性支持 |
微服务、云原生架构 |
常见的JVM有HotSpot、GraalVM、OpenJ9等,另外DragonWell龙井JDK也提供了一款功能增强版的JVM。
其中使用最广泛的是HotSpot虚拟机
可以java -version
命令查看你目前使用的JVM虚拟机,如下图所示:
1.3 常见的JVM: Java虚拟机规范
- 《Java虚拟机规范》由Oracle制定,内容主要包含了Java虚拟机在设计和实现时需要遵守的规范,主
要包含class字节码文件的定义、类和接口的加载和初始化、指令集等内容。 - 《Java虚拟机规范》是对虚拟机设计的要求,而不是对Java设计的要求,也就是说虚拟机可以运行在
其他的语言比如Groovy、Scala生成的class字节码文件之上。 - 官网地址:https://docs.oracle.com/javase/specs/index.html
1.4 常见的JVM - HotSpot的发展历程
2 JVM的组成
瑞:由于JVM的组成内容相对多,具体讲解会在本系列后续的篇章更新,此处只放组成图
3 字节码文件的打开方式
3.1 以正确的姿势打开字节码.class文件
经过 Java 编译器编译 Java 源文件后的产物就是字节码文件,后缀为.class
。字节码文件中保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读。
3.1.1 NotePad++的插件HexEditor
当然,可以通过NotePad++使用十六进制插件HexEditor
查看class文件,如下图所示:
3.1.2 jclasslib
正确的打开.class字节码文件的姿势,安装 jclasslib工具查看字节码文件,下载后安装,预览一个磁盘位置,然后一直无脑下一步就行了
- Github地址: https://github.com/ingokegel/jclasslib
安装成功后,再次选择RayTest.class字节码文件,使用jclasslib打开的效果如下图所示,直观很多
3.1.3 IDEA插件jclasslib
使用IDEA的小伙伴可以直接 settings -> Plugins 中搜索jclsslib Bytecode Viewer
并下载,图标如下图所示
下载后就可以选择已编译后的类
,找到工具栏中的View
选项卡,找到Show Bytecode With Jclasslib
选项打开字节码文件,如下图所示:
4 字节码文件的组成
组成 | 描述 |
---|---|
基本信息 | 魔数、字节码文件对应的Java版本号 访问标识(public final等等) 父类和接口 |
常量池 | 保存了字符串常量、类或接口名、字段名 主要在字节码指令中使用 |
字段 | 当前类或接口声明的字段信息 |
方法 | 当前类或接口声明的方法信息 字节码指令 |
属性 | 类的属性,比如源码的文件名 内部类的列表等 |
4.0 常见的字节码指令(持续更新…)
- iconst_常量值:将常量值放到操作数栈中(临时存放)。生成常量。
- istore_数组下标:将操作数栈中的值取出放入局部变量表中下标位置。本身(操作数栈中)没有了。赋值语句。
- iload_数组下标:将局部变量表中的数据放入操作数栈中。是复制一份到操作数栈中,本身还在。要执行操作语句的准备。
- putstatic:给类中的静态字段赋值,值从操作数栈中获取
- iadd:操作是在操作数栈中进行的
- iinc n by n:把n加上n,如iinc 1 by 1,把1加上1,此操作不在操作数栈中进行,直接在局部变量数组表中进行。++操作
- ldc #2 <1> 从字符串常量池中获取字符串为”1”的地址放入操作数栈中
- astore_1:将操作数栈中的值放入局部变量表中下标为1的变量
invokestatic和invokevirtual是Java字节码中的两种指令,用于调用方法。它们的区别如下:
1️⃣调用方式不同:
invokestatic用于调用静态方法,即通过类名直接调用的方法。它不需要创建类的实例,因此不需要访问对象的字段和方法。
invokevirtual用于调用虚方法,即通过对象实例调用的方法。它需要访问对象的字段和方法,因此需要创建类的实例。
2️⃣符号引用不同:
invokestatic的符号引用表示的是静态方法的名称和描述符,格式为<classname>.<methodname>(<parameter_types>)。
invokevirtual的符号引用表示的是虚方法的名称和描述符,格式为<classname>.super::<methodname>(<parameter_types>)或<classname>::<methodname>(<parameter_types>)。其中,super::表示调用父类的同名方法,::表示调用当前类的同名方法。
3️⃣参数传递不同:
invokestatic的参数传递是通过操作数栈进行的,将参数按照顺序压入栈中,并在调用方法时弹出相应的值。
invokevirtual的参数传递也是通过操作数栈进行的,但需要在参数之前先压入一个对当前对象实例的引用(通常称为对象指针)。在调用方法时,会使用该引用来访问对象的字段和方法。
4️⃣性能差异:
invokestatic的性能通常比invokevirtual要好一些,因为它不需要创建类的实例,也不需要访问对象的字段和方法。它的调用开销较小,执行速度较快。
invokevirtual的性能相对较差,因为它需要创建类的实例,并访问对象的字段和方法。这会增加额外的开销和执行时间。
综上所述,invokestatic用于调用静态方法,而invokevirtual用于调用虚方法。它们在调用方式、符号引用、参数传递和性能等方面存在一些区别。
4.1 字节码文件的组成——基本信息
基本信息包含:
魔数、字节码文件对应的Java版本号、访问标识(public final等等)、父类和接口
名称 | 作用 |
---|---|
Magic魔数 | 固定为0xCAFEBABE,不会改变 |
副版本号 | 编译字节码文件的JDK版本 |
主版本号 | 编译字节码文件的JDK版本 |
访问标识 | 标识是类还是接口、注解、枚举、模块 标识public final abstract |
类、父类、接口索引 | 通过这些索引可以找到类、父类、接口的信息 |
4.1.1 基本信息——Magic魔数
- 文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。
- 软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错。
- Java字节码文件中,将文件头称为magic魔数。
文件类型 | 字节数 | 文件头 |
---|---|---|
JPEG (jpg) | 3 | FFD8FF |
PNG (png) | 4 | 89504E47(文件尾也有要求) |
bmp | 2 | 424D |
XML (xml) | 5 | 3C3F786D6C |
AVI (avi) | 4 | 41564920 |
Java字节码文件(.class) | 4 | CAFEBABE |
我们以.class的Java字节码文件为例,通过NotePad++使用十六进制插件HexEditor
查看任意class文件,会发现,头四个字节都是cafebabe,如下图所示:
4.1.2 基本信息——主副版本号
- 主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。
- 版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容。
1.2之后大版本号计算方法就是:主版本号 – 44。比如主版本号52就是JDK1.8
4.1.2.1 主版本号不兼容导致的错误
案例:主版本号不兼容导致的错误
需求:解决以下由于主版本号不兼容导致的错误
类文件具有错误的版本 52.0,应为 50.0
请删除该文件或确保该文件应位于正确的类路径子目录中。
如:由于某个依赖(如commons.lang3)需要JDK8(52-44=8),但是运行时环境是JDK6(50-44=6),就会报如上红色的错误。
两种解决方案:
1.升级JDK版本(容易引发其他的兼容性问题,并且需要大量的测试)
2.将第三方依赖的版本号降低或者更换依赖,以满足JDK版本的要求 √ 建议采用
4.2 字节码文件的组成部分——常量池
常量池中:保存了字符串常量、类或接口名、字段名。主要在字节码指令中使用
- 字节码文件中常量池的作用:避免相同的内容重复定义,节省空间。
- 常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。
- 字节码指令中通过编号引用到常量池的过程称之为符号引用。
4.3 字节码文件的组成部分——方法
方法:当前类或接口声明的方法信息字节码指令
- 字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中
- 操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置
4.3.1 面试题
以下代码的运行结果是什么?
public static void main(String[] args) {
int i = 0;
i = i++;
System.out.println(i); // 0
}
虽然我们知道结果为0,可是你以前思考过为什么是0吗?在面试的时候如果能通过字节码向面试官解释原因,是不是效果很好。下面我们就使用字节码文件对结果进行分析
我们以i++
和++i
观察字节码指令执行流程
先观察i=i++
,为了方便,博主使用IDEA插件jclasslib展示
public class RayTest {
public static void main(String[] args) {
int i = 0;
i = i++;
}
}
再观察i=++i
两者的区别:
i++操作是先执行iload_1:将局部变量表中下标为1的数据(int i=0)也就是当前0放入操作数栈中,然后执行iicn 1 by 1,由于该操作数在局部变量数组表中进行,也就是局部变量数组表中的i是加1了,但是后面又执行了istore_1操作,把操作数栈中的0赋值给了局部变量数组下标为1的i,使得i又变回了0
而++i操作数先执行iicn 1 by 1,由于该操作数在局部变量数组表中进行,也就是局部变量数组表中的i加1,然后执行iload_1,将局部变量表中的i=1的值放入操作数栈中,再执行istore_1操作,把操作数栈中的1赋值给了局部变量数组下标为1的i,使得i等于1
通过分析方法中的字节码指令,我们就知道了为什么最终输出的结果为0,通过分析字节码指令发现,i++先把0取出来放入临时的操作数栈中,接下来对i进行加1,i变成了1,最后再将之前保存的临时值0放入i,最后i就变成了0。
如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏⭐️转发🔗评论📝都是对博主最好的支持~