瑞_JVM虚拟机_概述

文章目录

    • 1 什么是JVM
      • 1.1 JVM功能
      • 1.2 常见的JVM
      • 1.3 常见的JVM: Java虚拟机规范
      • 1.4 常见的JVM - HotSpot的发展历程
    • 2 JVM的组成
    • 3 字节码文件的打开方式
      • 3.1 以正确的姿势打开字节码.class文件
        • 3.1.1 NotePad++的插件HexEditor
        • 3.1.2 jclasslib
        • 3.1.3 IDEA插件jclasslib
    • 4 字节码文件的组成
      • 4.0 常见的字节码指令(持续更新...)
      • 4.1 字节码文件的组成——基本信息
        • 4.1.1 基本信息——Magic魔数
        • 4.1.2 基本信息——主副版本号
          • 4.1.2.1 主版本号不兼容导致的错误
      • 4.2 字节码文件的组成部分——常量池
      • 4.3 字节码文件的组成部分——方法
        • 4.3.1 面试题

前言:本文章为瑞_系列专栏之《JVM虚拟机》的概述篇,本篇章主要介绍什么是JVM、JVM功能、JVM的组成以及字节码文件的组成。由于博主是从B站黑马程序员的《JVM虚拟机》学习到的相关知识,所以本系列专栏主要针对该课程进行笔记总结和拓展,文中的部分原理及图解也是来源于黑马提供的资料。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!

瑞_JVM虚拟机_概述_第1张图片

1 什么是JVM

  JVM 全称是 Java Virtual Machine,中文译名“Java虚拟机”。
  JVM 本质上是一个运行在计算机上的程序,它的职责是运行Java字节码文件

瑞_JVM虚拟机_概述_第2张图片

瑞:虚拟机就是将字节码指令解释成机器码,机器码交给计算机运行

1.1 JVM功能

  1. 解释和运行:对字节码文件中的指令,实时的解释成机器码,让计算机执行
  2. 内存管理:自动为对象、方法等分配内存。自动的垃圾回收机制,回收不再使用的对象
  3. 即时编译:对热点代码进行优化,提升执行效率

瑞_JVM虚拟机_概述_第3张图片

  Java需要实时解释,主要是为了支持跨平台特性

瑞_JVM虚拟机_概述_第4张图片

  所以 Java语言如果不做任何优化,性能不如C、C++等语言

瑞_JVM虚拟机_概述_第5张图片

  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虚拟机,如下图所示:

瑞_JVM虚拟机_概述_第6张图片

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的发展历程

瑞_JVM虚拟机_概述_第7张图片



2 JVM的组成

瑞_JVM虚拟机_概述_第8张图片

瑞:由于JVM的组成内容相对多,具体讲解会在本系列后续的篇章更新,此处只放组成图




3 字节码文件的打开方式

3.1 以正确的姿势打开字节码.class文件

  经过 Java 编译器编译 Java 源文件后的产物就是字节码文件,后缀为.class。字节码文件中保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读。

3.1.1 NotePad++的插件HexEditor

  当然,可以通过NotePad++使用十六进制插件HexEditor查看class文件,如下图所示:

瑞_JVM虚拟机_概述_第9张图片

3.1.2 jclasslib

  正确的打开.class字节码文件的姿势,安装 jclasslib工具查看字节码文件,下载后安装,预览一个磁盘位置,然后一直无脑下一步就行了

  • Github地址: https://github.com/ingokegel/jclasslib

  安装成功后,再次选择RayTest.class字节码文件,使用jclasslib打开的效果如下图所示,直观很多

瑞_JVM虚拟机_概述_第10张图片

3.1.3 IDEA插件jclasslib

  使用IDEA的小伙伴可以直接 settings -> Plugins 中搜索jclsslib Bytecode Viewer并下载,图标如下图所示

瑞_JVM虚拟机_概述_第11张图片
  下载后就可以选择已编译后的类,找到工具栏中的View选项卡,找到Show Bytecode With Jclasslib选项打开字节码文件,如下图所示:

瑞_JVM虚拟机_概述_第12张图片




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的符号引用表示的是静态方法的名称和描述符,格式为.()。
    invokevirtual的符号引用表示的是虚方法的名称和描述符,格式为.super::()或::()。其中,super::表示调用父类的同名方法,::表示调用当前类的同名方法。
  3️⃣参数传递不同:
    invokestatic的参数传递是通过操作数栈进行的,将参数按照顺序压入栈中,并在调用方法时弹出相应的值。
    invokevirtual的参数传递也是通过操作数栈进行的,但需要在参数之前先压入一个对当前对象实例的引用(通常称为对象指针)。在调用方法时,会使用该引用来访问对象的字段和方法。
  4️⃣性能差异:
    invokestatic的性能通常比invokevirtual要好一些,因为它不需要创建类的实例,也不需要访问对象的字段和方法。它的调用开销较小,执行速度较快。
    invokevirtual的性能相对较差,因为它需要创建类的实例,并访问对象的字段和方法。这会增加额外的开销和执行时间。
  综上所述,invokestatic用于调用静态方法,而invokevirtual用于调用虚方法。它们在调用方式、符号引用、参数传递和性能等方面存在一些区别。

4.1 字节码文件的组成——基本信息

  基本信息包含:魔数、字节码文件对应的Java版本号、访问标识(public final等等)、父类和接口

名称 作用
Magic魔数 固定为0xCAFEBABE,不会改变
副版本号 编译字节码文件的JDK版本
主版本号 编译字节码文件的JDK版本
访问标识 标识是类还是接口、注解、枚举、模块
标识public final abstract
类、父类、接口索引 通过这些索引可以找到类、父类、接口的信息

瑞_JVM虚拟机_概述_第13张图片

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,如下图所示:

瑞_JVM虚拟机_概述_第14张图片

4.1.2 基本信息——主副版本号
  • 主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。
  • 版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容。

瑞_JVM虚拟机_概述_第15张图片

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开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。
  • 字节码指令中通过编号引用到常量池的过程称之为符号引用。

瑞_JVM虚拟机_概述_第16张图片

4.3 字节码文件的组成部分——方法

方法:当前类或接口声明的方法信息字节码指令

  • 字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中
  • 操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置

瑞_JVM虚拟机_概述_第17张图片

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++;
    }
}

瑞_JVM虚拟机_概述_第18张图片

  再观察i=++i

瑞_JVM虚拟机_概述_第19张图片

  两者的区别:

  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。




本文是博主的粗浅理解,可能存在一些错误或不完善之处,如有遗漏或错误欢迎各位补充,谢谢

  如果觉得这篇文章对您有所帮助的话,请动动小手点波关注,你的点赞收藏⭐️转发评论都是对博主最好的支持~


你可能感兴趣的:(#,jvm,jvm,java)