🙊前言:本文章为瑞_系列专栏之《JVM虚拟机》的概述篇,本篇章主要介绍什么是JVM、JVM功能、JVM的组成以及字节码文件的组成。由于博主是从B站黑马程序员的《JVM虚拟机》学习到的相关知识,所以本系列专栏主要针对该课程进行笔记总结和拓展,文中的部分原理及图解也是来源于黑马提供的资料。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!
??JVM 全称是 Java Virtual Machine,中文译名“Java虚拟机”。
??JVM 本质上是一个运行在计算机上的程序,它的职责是运行Java字节码文件。
瑞:虚拟机就是将字节码指令解释成机器码,机器码交给计算机运行
??Java需要实时解释,主要是为了支持跨平台特性
??所以 Java语言如果不做任何优化,性能不如C、C++等语言
??JVM提供了即时编译(Just-In-Time 简称JIT) 进行性能的优化,最终能达到接近C、C++语言的运行性能,甚至在特定场景下实现超越
名称 | 作者 | 支持版本 | 社区活跃度 (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虚拟机,如下图所示:
瑞:由于JVM的组成内容相对多,具体讲解会在本系列后续的篇章更新,此处只放组成图
??经过 Java 编译器编译 Java 源文件后的产物就是字节码文件,后缀为.class
。字节码文件中保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读。
??当然,可以通过NotePad++使用十六进制插件HexEditor
查看class文件,如下图所示:
??正确的打开.class字节码文件的姿势,安装 jclasslib工具查看字节码文件,下载后安装,预览一个磁盘位置,然后一直无脑下一步就行了
??安装成功后,再次选择RayTest.class字节码文件,使用jclasslib打开的效果如下图所示,直观很多
??使用IDEA的小伙伴可以直接 settings -> Plugins 中搜索jclsslib Bytecode Viewer
并下载,图标如下图所示
??下载后就可以选择已编译后的类
,找到工具栏中的View
选项卡,找到Show Bytecode With Jclasslib
选项打开字节码文件,如下图所示:
组成 | 描述 |
---|---|
基本信息 | 魔数、字节码文件对应的Java版本号 访问标识(public final等等) 父类和接口 |
常量池 | 保存了字符串常量、类或接口名、字段名 主要在字节码指令中使用 |
字段 | 当前类或接口声明的字段信息 |
方法 | 当前类或接口声明的方法信息 字节码指令 |
属性 | 类的属性,比如源码的文件名 内部类的列表等 |
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用于调用虚方法。它们在调用方式、符号引用、参数传递和性能等方面存在一些区别。
??基本信息包含:
魔数、字节码文件对应的Java版本号、访问标识(public final等等)、父类和接口
名称 | 作用 |
---|---|
Magic魔数 | 固定为0xCAFEBABE,不会改变 |
副版本号 | 编译字节码文件的JDK版本 |
主版本号 | 编译字节码文件的JDK版本 |
访问标识 | 标识是类还是接口、注解、枚举、模块 标识public final abstract |
类、父类、接口索引 | 通过这些索引可以找到类、父类、接口的信息 |
文件类型 | 字节数 | 文件头 |
---|---|---|
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,如下图所示:
1.2之后大版本号计算方法就是:主版本号 – 44。比如主版本号52就是JDK1.8
??案例:主版本号不兼容导致的错误
??需求:解决以下由于主版本号不兼容导致的错误
????类文件具有错误的版本 52.0,应为 50.0
????请删除该文件或确保该文件应位于正确的类路径子目录中。
??如:由于某个依赖(如commons.lang3)需要JDK8(52-44=8),但是运行时环境是JDK6(50-44=6),就会报如上红色的错误。
??两种解决方案:
????1.升级JDK版本(容易引发其他的兼容性问题,并且需要大量的测试)
????2.将第三方依赖的版本号降低或者更换依赖,以满足JDK版本的要求 √ 建议采用
常量池中:保存了字符串常量、类或接口名、字段名。主要在字节码指令中使用
方法:当前类或接口声明的方法信息字节码指令
??以下代码的运行结果是什么?
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。
??如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏??转发🔗评论📝都是对博主最好的支持~