JVM基础:字节码文件详解①

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 一、Java虚拟机的组成
  • 二、字节码文件的组成
    • [2.1 为什么要了解字节码文件?](#2.1 为什么要了解字节码文件?)
    • [2.2 如何"窥探"字节码文件的奥秘?](#2.2 如何“窥探”字节码文件的奥秘?)
      • [2.2.1 使用工具打开字节码文件](#2.2.1 使用工具打开字节码文件)
      • [2.2.2 字节码文件是由哪几部分组成?](#2.2.2 字节码文件是由哪几部分组成?)
      • [2.2.3 基础信息(一般信息)](#2.2.3 基础信息(一般信息))
      • [2.2.4 常量池](#2.2.4 常量池)
      • [2.2.5 方法](#2.2.5 方法)
  • 参考目录

提示:以下是本篇文章正文内容,下面案例可供参考

一、Java虚拟机的组成

👉它可以分为以下四个部分

  1. 类加载器(ClassLoder)加载class字节码文件中的内容到内存中(加载到内存是为了高效的利用)
  2. 运行时数据区域(JVM管理的内存)负责管理JVM使用到的内存,比如创建对象和销毁对象
  3. 执行引擎((即时编译器、解释器与垃圾回收器等)将字节码文件中的指令解
    释成机器码,同时使用即时编译器优化性能
  4. 本地接口调用本地已经编译的方法,比如虚拟机中提供的c/c++的方法,就是本地方法(即jvm底层已经实现好的,用C/C++语言编写好的方法,使用native修饰的方法)

👉基本执行流程如下所示:


二、字节码文件的组成

2.1 为什么要了解字节码文件?

👉原因

①它可以解决一些面试难题

例如以下面试题

  • int i = 0; i = i++; 最终i的值是多少?
  • 请你回答一下Java的反射是如何实现的?

②它可以解决工作中的一些实际问题------版本冲突

例如以下报错

2.2 如何"窥探"字节码文件的奥秘?

2.2.1 使用工具打开字节码文件

👉常用工具

①使用Jclasslib字节码插件【idea插件】

如何使用该插件查看指定字节码文件?

👉步骤

①在idea中 file -- settings -- plugins 中搜索 jclass 安装该插件

②选中指定的Java源文件,按照以下步骤操作,即可

👉注意

使用jclasslib的idea插件版的两个小细节

  1. 要打开一个新的字节码文件,就需要选中当前的源代码,然后点击 view - show bytecode with jclasslib 就可以打开新的字节码文件


    2. 如果源代码发生变化,需要重新运行编译,可通过 build - Recompile 'xxx.java' 或 重新运行该源代码的方式重新编译,然后在jclasslib中重新刷新即可

②使用Javap命令

如何使用该命令查看指定字节码文件?

👉步骤

  1. 首先确保已经安装了JDK(Java Development Kit),因为Javap是JDK的一部分。

  2. 打开命令提示符(Windows)或终端(macOS/Linux)。

  3. 使用cd命令导航到包含字节码文件(.class文件)的目录。例如,如果字节码文件位于C:\Users\YourUsername\Documents\MyProject目录下,请输入cd C:\Users\YourUsername\Documents\MyProject

  4. 输入javap -c YourClassName.class命令,其中YourClassName是你要查看的字节码文件的名称(不包括扩展名)。例如,如果你要查看名为MyClass.class的文件,请输入javap -c MyClass.class

  5. 按回车键执行命令。命令将显示字节码文件的详细信息,包括类名、方法名、参数类型等。

案例:使用Javap命令打开桌面测试文件夹中的字节码文件t1.class

👉备注

如果jar包需要先使用 jar --xvf 命令解压
③使用Arthas工具打开字节码文件

GitHub地址

2.2.2 字节码文件是由哪几部分组成?

使用jclass插件打开任意一个Java 源文件,我们可以看到如下信息

  • 一般信息(基本信息)魔数、字节码文件对应的Java版本号,访问标识(public final等等)以及父类和接口

  • 常量池保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用

  • 接口当前类实现的接口信息

  • 字段当前类或接口声明的字段信息

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

  • 属性类的属性,比如源码的文件名,内部类的列表等

2.2.3 基础信息(一般信息)

前面提到的基本信息中主要包含魔数、字节码文件对应的Java版本号,访问标识(public final等等)以及父类和接口等内容,但是有两个问题值得深思

😥问题①:何为魔数?

使用notepad++ 随便打开两个字节码文件,我们可以看到他们之间显著的共通之处

a.打开t1.class字节码文件,如下图所示

b.打开MyApplication.class字节码,如下图所示

共通之处

以ca fe ba be打头

这就是魔数(Magic),用以校验文件类型的文件头,如果别的编译软件不支持该种类型的文件头,则解析文件时会出错。那为什么不使用文件扩展名去校验类型?文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。故而在Java字节码文件中,将文件头称为魔数

😥问题②:何为Java版本号?

Java版本号主要分为主副版本号,主副版本号指的是编译字节码文件的JDK版本号,主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。

👉版本号作用

主要判断当前字节码的版本和运行时的JDK是否兼容

👉备注

1.2之后大版本号计算方法就是:主版本号 -- 44 ;
比如主版本号52,那就是JDK8

之前在前文中抛出了一个版本冲突的问题

如下图所示

👉原因

主版本号不兼容,发生冲突

👉解决方案

1.升级JDK版本(容易引发其他的兼容性问题,并且需要大量的测试)

2.将第三方依赖的版本号降低或者更换依赖,以满足JDK版本的要求 √ 建议采用

👉总结

2.2.4 常量池

👉作用

避免相同的内容重复定义,节省空间

👉概述

  • 常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据
  • 字节码指令中通过编号引用到常量池的过程称之为符号引用

👉符号引用的示意图如下所示

2.2.5 方法

👉先看一个简单经典的面试题

int i = 0; i = i++; 最终i的值是多少?

👉我的回答

i =i++ 是先赋值后自增,而i= ++ i 是先自增后赋值,所以最终的i是0

😚进一步发问

为什么i =i++ 是先赋值后自增,而i= ++ i 是先自增后赋值,如果根据Java的运算符优先级对比,应该是1吧,i++优先级高,先执行之后将返回结果1赋值给 i,所以最终 i应该是1。

可正确的答案是 i最终的值是0

😥why?

莫急,且听我慢慢道来

👉定义

字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中

选中Code属性,我们可以看到它下面还有两个Table

  • LineNumberTable用于存储源代码中各行号与字节码指令之间的对应关系,它可以帮助调试器在执行程序时定位到源代码中的具体位置。

  • LocalVariableTable主要用于存储方法的参数和方法内定义的局部变量。在程序编译为Class文件时,会在Code属性的max_locals数据项中确定该方法所需要分配的局部变量表的最大容量。

我们暂时只关心LocalVariableTable,如下图所示,LocalVariableTable表中的每个项都包含以下内容:

  • 名称(Name)表示该项对应的局部变量的名称
  • 描述符(Descriptor)表示该项对应的局部变量的类型和修饰符
  • 索引(Slot)表示该项在局部变量表中的位置
  • 值(Value)表示该项对应的局部变量的值

除了上面的LocalVariableTable,我们还得了解一个概念------操作数栈

😥什么是操作数栈?

👉讯飞星火告诉我们

简单来讲,操作数栈是临时存放数据的地方

👉再看之前的源代码

java 复制代码
 int i=0;
 int j= i+1;

字节码指令分析如下

回到前面提到的面试题

😥为什么int i = 0; i = i++; 最终i的值是0?

源代码示例如下

java 复制代码
public static void main(String[] args) {
        int i=0;
        i=i++;
        System.out.println("i = " + i);
}

👉分析流程如下所示

👉举例分析

以 int i=0; i=++i; 的字节码指令展开分析,最后i的值是多少?

示例代码如下

java 复制代码
 public static void main(String[] args) {
        int i=0;
        i=++i;
        System.out.println("i = " + i);
 }

字节码指令如下

c 复制代码
 0 iconst_0
 1 istore_1
 2 iinc 1 by 1
 5 iload_1
 6 istore_1
 7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 new #3 <java/lang/StringBuilder>
13 dup
14 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
17 ldc #5 <i = >
19 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
22 iload_1
23 invokevirtual #7 <java/lang/StringBuilder.append : (I)Ljava/lang/StringBuilder;>
26 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
29 invokevirtual #9 <java/io/PrintStream.println : (Ljava/lang/String;)V>
32 return

👉流程分析如下

①将int类型的 o push 操作数栈中;
②从操作数栈中取出0,放入到局部变量表中位序为1的位置上[i],此时i=0;
③在局部变量表中为位序为1的位置上增加1,此时i=1;
④从局部变量表中位序为1的位置将数据压入到操作数栈中,此时i=0;
⑤将操作数栈中的数据[1]保存到局部变量表中位序为1的位置上,此时i=1;

...

👉备注

如果不清楚某一条指令的作用,可采取以下步骤

①选中指令,点击"显示JVM规范"

②浏览器会自动跳转至对应指令的详情页面


参考目录

https://www.bilibili.com/video/BV1r94y1b7eS?p=7\&spm_id_from=pageDriver\&vd_source=5a34715e416a427a73a3ca52397848b5


相关推荐
奋进的芋圆1 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin2 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20052 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉2 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国2 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882483 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈3 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_993 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹3 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理
专注_每天进步一点点3 小时前
【java开发】写接口文档的札记
java·开发语言