16 进制打开class文件
可以通过Notepad++下载一个HexEditor插件,下载好该插件后可以以16进制的方式打开class看,打开后我们可以看到如下所示的图片:
class 文件的组成
class 文件的组成部分为:魔数,版本号,常量池,字段表集合,方法表集合
Java字节码分析
通过javap -v Java文件名称.class 我们可以获取到Java的class字节码的内容,例如获取JvmTest.class文件的字节码信息:
javap -v JvmTest.class
Java源文件内容如下:
package com.test.jvm;
import com.test.entity.ExportDataPushLog;
/**
*
* @author JR
* @version 1.0
* @description:
* @date 2024年06月26日 14:58
*/
public class JvmTest implements JvmTestInterface {
private static int a = 1;
private static int b;
// 注意如果使用javap -v 命令这里如果是private则这里在class文件中不显示的
// 所以这里用的public,目前还不明白为什么这么做的原因,需要回头查查资料
public static final String str = "123";
public static void main(String[]args) {
int i = 0;
int j = 0;
i = 1;
}
public static ExportDataPushLog A(String t1, int t2) {
System.out.println("A执行了..");
B();
return null;
}
public static void B() {
System.out.println("B执行了..");
C();
}
public static void C() {
System.out.println("C执行了.·.");
}
}
获取到的字节码信息如下:
// 第一行文件指定了这个class文件在当前系统中的绝对路径
Classfile /C:/Users/Administrator/Desktop/my-file/Test/target/classes/com/test/jvm/JvmTest.class
// 最后的修改时间 文件大小
Last modified 2024年7月1日; size 1084 bytes
// 这个不知道没看懂
SHA-256 checksum acf3be9291a67e6c4671e6a8639b26b99209979da771e519bb8fc38fa90064ed
Compiled from "JvmTest.java"
public class com.test.jvm.JvmTest implements com.test.jvm.JvmTestInterface
// 主版本号
minor version: 0
// 副版本号
major version: 55
// 当前class 文件的标识,下面图片有相关说明继续往后看
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
// 说明了当前是哪个类
this_class: #10 // com/test/jvm/JvmTest
// 当前类的父类
super_class: #11 // java/lang/Object
// 实现了一个接口,类中有3个成员变量,六个方法
// 至于attributes暂时不理解,后面在找找资料,主要不理解这里的fields和attributes
// 按道理字段就应该是属性,但是这里却是fields是3,而attributes:1
interfaces: 1, fields: 3, methods: 6, attributes: 1
Constant pool:
#1 = Methodref #11.#40 // java/lang/Object."<init>":()V
#2 = Fieldref #41.#42 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #43 // A执行了..
#4 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Methodref #10.#46 // com/test/jvm/JvmTest.B:()V
#6 = String #47 // B执行了..
#7 = Methodref #10.#48 // com/test/jvm/JvmTest.C:()V
#8 = String #49 // C执行了.·.
#9 = Fieldref #10.#50 // com/test/jvm/JvmTest.a:I
#10 = Class #51 // com/test/jvm/JvmTest
#11 = Class #52 // java/lang/Object
#12 = Class #53 // com/test/jvm/JvmTestInterface
#13 = Utf8 a
#14 = Utf8 I
#15 = Utf8 b
#16 = Utf8 str
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/test/jvm/JvmTest;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 i
#30 = Utf8 j
#31 = Utf8 A
#32 = Utf8 (Ljava/lang/String;I)Lcom/test/entity/ExportDataPushLog;
#33 = Utf8 t1
#34 = Utf8 t2
#35 = Utf8 B
#36 = Utf8 C
#37 = Utf8 <clinit>
#38 = Utf8 SourceFile
#39 = Utf8 JvmTest.java
#40 = NameAndType #18:#19 // "<init>":()V
#41 = Class #54 // java/lang/System
#42 = NameAndType #55:#56 // out:Ljava/io/PrintStream;
#43 = Utf8 A执行了..
#44 = Class #57 // java/io/PrintStream
#45 = NameAndType #58:#59 // println:(Ljava/lang/String;)V
#46 = NameAndType #35:#19 // B:()V
#47 = Utf8 B执行了..
#48 = NameAndType #36:#19 // C:()V
#49 = Utf8 C执行了.·.
#50 = NameAndType #13:#14 // a:I
#51 = Utf8 com/test/jvm/JvmTest
#52 = Utf8 java/lang/Object
#53 = Utf8 com/test/jvm/JvmTestInterface
#54 = Utf8 java/lang/System
#55 = Utf8 out
#56 = Utf8 Ljava/io/PrintStream;
#57 = Utf8 java/io/PrintStream
#58 = Utf8 println
#59 = Utf8 (Ljava/lang/String;)V
{
// 字段表集合,这里一定要注意,一定要是public 否则的话使用Javap在class文件中是不显示的
// 问题:问什么使用private在class文件中是不显示的?
public static final java.lang.String str;
descriptor: Ljava/lang/String;
// 首先是public的,其次是静态字段,其次是常量字段
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String 123
// 方法信息
public com.test.jvm.JvmTest();
// ()V代表入参和返回值,这里没有入参,返回值也没有所以入参是:()出餐是:V,
descriptor: ()V
// 方法是public
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/test/jvm/JvmTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
// 问题1:编译器是如何计算出栈的最大深度,本地变量表的最大容量的
// 问题2:这里的槽的概念是什么
// 方法的Code属性的一部分,具体说明了该方法在执行时的栈帧布局
// stack=1: 这表示在执行该方法时,Java虚拟机(JVM)为它分配的栈的最大深度是1。栈深是指方法调用过程中,
// 操作数栈所能容纳的最大元素数量。这个值由编译器计算得出,确保了足够的空间来存放方法执行过程中的中间结果和操作数,而又不会过多地占用资源。
// locals=1: 这里指的是局部变量表的最大容量是1。局部变量表用于存储方法参数和方法内部定义的局部变量。这个值同样由编译器计算确定,
// 确保有足够的槽位来保存所有可能同时存活的局部变量和方法参数。在这个例子中,有一个局部变量槽被分配。
// args_size=1: 这个参数说明了该方法接收的参数数量。在这里,方法有一个参数。需要注意的是,对于实例方法,
// 局部变量表的第一个槽位默认用于存储this引用(指向当前对象实例),但这不在args_size中计算。
// 因此,如果这是一个实例方法且args_size=1,实际上局部变量表会有两个槽位:一个用于this,另一个用于传递给方法的参数。
stack=1, locals=3, args_size=1
// 在iconst_0中这里的0是指什么?在istore_1中这里的1又是什么
0: iconst_0
// 对变量i进行赋值,并弹栈
1: istore_1
2: iconst_0
3: istore_2
4: iconst_1
5: istore_1
6: return
// 行号对应的Nr
LineNumberTable:
line 20: 0
line 21: 2
line 23: 4
line 24: 6
// 本地变量表(局部变量表)
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
2 5 1 i I
4 3 2 j I
public static com.test.entity.ExportDataPushLog A(java.lang.String, int);
descriptor: (Ljava/lang/String;I)Lcom/test/entity/ExportDataPushLog;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String A执行了..
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: invokestatic #5 // Method B:()V
11: aconst_null
12: areturn
LineNumberTable:
line 26: 0
line 27: 8
line 28: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 t1 Ljava/lang/String;
0 13 1 t2 I
public static void B();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String B执行了..
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: invokestatic #7 // Method C:()V
11: return
LineNumberTable:
line 32: 0
line 33: 8
line 34: 11
public static void C();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String C执行了.·.
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 36: 0
line 37: 8
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #9 // Field a:I
4: return
LineNumberTable:
line 14: 0
}
SourceFile: "JvmTest.java"
问题:为什么没有看到字段表集合
class 文件中flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT相关说明
class文件中的基本信息
class文件魔数
class文件魔数就是指class文件开头的ca fe ba be 联合起来是咖啡馆的意思,魔数(Magic Number)用于辨别当前文件是否是JavaClass 文件,例如:将class 文件的后缀".class"更改为**“.jpg”**扩展名可以随意修改不影响文件的内容,例如:有一张图片a.png 当我们尝试将后缀名称png 改为avi 此时发现使用图片打开工具仍然能够打开该文件,这说明文件后缀名的改变并不能实际改变文件的类型,一般软件通过文件开头的几个字节来分别文件的类型,如果不支持该类型就会报错,而开头的这个字节被称为魔数,以下是一些常见的文件类型及其对应的魔数:
- JPEG 图像文件:FF D8 FF
- PNG 图像文件:89 50 4E 47 0D 0A 1A 0A
- GIF 图像文件:47 49 46 38 39 61 或 47 49 46 38 37 61
- BMP 图像文件:42 4D
- WAV 音频文件:52 49 46 46(RIFF)和57 41 56 45(WAVE)
- MP3 音频文件:FF FB(MPEG-1 layer III)或 49 44 33(ID3)
- MP4 视频文件:00 00 00 18 66 74 79 70 6D 70 34 32 00 00 00 00
- AVI 视频文件:52 49 46 46(RIFF)和41 56 49 20(AVI )
- PDF 文件:25 50 44 46(%PDF)
- DOC 文件:D0 CF 11 E0 A1 B1 1A E1
class文件主副版本号
就是class文件在编译时,jdk的版本号,在魔数之后紧跟着魔数之后从第4个字节开始就是Class文件的版本号。第5和第6个字节所代表的含义是编译的副版本号,第7和第8个字节代表的是编译的主版本号。Class文件的版本号有主版本号.副版本号组成。
一般只需要关注主版本号,如上图所示上面7和8显示的版本号是37,37是十六进制转为10进制是55 使用55 减去44 等于11 所以当前的jdk 版本就是jdk 11,而副版本号则是作为主版本号相同时进行区分的作用,主版本号的作用一般是用来判断当前字节码的版本和运行时的jdk是否兼容,如果不兼容则有可能出现类似以下错误:
高版本的虚拟机可以执行低版本的编译器生成的Class文件,但是低版本的虚拟机不能执行高版本的编译成生成的Class文件。(向下兼容)
class文件中的常量池
常量池分为:静态常量池(class文件常量池),字符串常量池,运行时常量池,class文件中的常量池通常是指静态常量池和字符串常量池:
静态常量池(class文件常量池):静态常量池是相对于运行时常量池来说的,属于描述class文件结构的一部分,由字面量和符号引用组成,在类被加载后会将静态常量池加载到内存中也就是运行时常量池
**字符串常量池:**字符串作为最常用的数据类型,为减小内存的开销,专门为其开辟了一块内存区域(字符串常量池)用以存放。其中保存了某些字符的唯一值,例如:当创建一个变量a1 并赋值为:我爱北京天安门,首先会先判断字符串常量池中是否存在字符串内容为:我爱北京天安门的字符串(也就是字面量),常量池中每个字段都有一个编号是从1开始的,并且在常量池中每个常量池这里引用的是一个编号,这里称为常量值索引,每个字段引用的并不是实际的值,而是常量值索引,通过常量值索引就可以找到具体编号的字面量
常量池保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用,如果按照类型划分Class文件常量池主要存放两大常量:字面量和符号引用,字节码文件中常量池的作用主要是为了避免相同的内容重复定义,节省空间。例如存在以下两个字符:
public class ConstantPoolTest{
public static final String a1="我爱北京天安门;
public static final String a2="我爱北京天安门";
public static void main(String[]args){ConstantPoolTest constantPoolTest new ConstantPoolTest();
}
在字节码文件中可能会出现:
"我爱北京天安门”
"我爱北京天安门”
通过使用classlib来观察class文件
从而导致出现重复的数据占用大量的空间内容,我们可以通过jclasslib 来观察class 文件,常量池中每个字段都有一个编号是从1开始的,并且在常量池中每个常量池这里引用的是一个编号
当我们打开八号会发现这里引用的并不是实际的值而是27号的一个索引:
打开27号发现27号才真正记录了真正的字面量,这个字面量才记录了:我爱天安门的数据
前面说过这里设置了两个变量,a1 和a2,所以同样的a2的字面量同样也并不是直接保存的我爱北京天安门,同样也是和a1一样,保存的字面量索引,通过索引逐步找到27号来获取实际的值,这样做的好处是可以节省一定的空间,同时也为了更好的管理变量,另外常量池中的字面量可能被变量名引用,也可以作为变量值引用。
字段表集合
字段表(field_info)用来描述接口或类中声明的变量,字段包括类级别变量以及实例级别变量。但不包括方法内部声明的局部变量
方法表集合
当前类或接口声明的方法信息的字节码指令,可以通过以下方式在jclasslib中查看指定方法的字节码指令,并通过鼠标右键点击后显示的列表中选择显示JVM规范功能查看当前指定字节码指令的作用:
打开之后我们发现 iconst_0 的作用描述为:
要了解下面这段代码生成的字节码指令首先要了解以下两个部分:
public static void main(String[]args){
int i = 0;
i = i ++;
}
- **操作数栈:**这里的操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。是一个先进后出的一个结构,所以入栈也叫压栈,出栈也叫弹栈
- **局部变量表(本地变量表):**局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,局部变量表中的存放顺序是通过变量定义的顺序存放的,同时方法内的变量赋值也是通过局部变量进行的,例如上面的main 方法为例:
了解了局部变量表和操作数栈之后我们可以尝试理解一下上面main 方法的字节码指令,通过jclasslib 查询我们可以了解到:
- iconst_<i>(注意这里的”<i>“是一个变量值,例如下面的:iconst_0):将int常量<i>压入操作数栈。从上面的图片我们说过,局部变量表是按照变量的创建顺序来指定序号和位置的,所以这里是取出索引为零的
- istore_<n>(这里"n"也一样,是个变量):而这里的n则代表局部变量表的索引位置,也就是上面的序号的值,所以这里的操作就是将从操作数栈中将序号为n的数据取出(也就是弹栈),放入局部变量表中,并对指定的变量进行赋值
- iload_1:将局部变量表中下标为n的变量的值放入操作数栈中
- iinc 1 by 1: 该方法直接操作局部变量表,直接加一
0 iconst_0 将零放入操作数栈中
1 istore_1 此时操作数栈中的值为0,并找到局部变量表中下标为1的变量,将值赋值给该变量 也就相当于i = 0
2 iload_1 此时局部变量表中下标为1的变量是"i",将该操作变量值推入操作数栈
3 iinc 1 by 1 直接操作局部变量表,加一,此时i = 1
6 istore_1 此时操作数栈中的值为0,将操作数栈中的值弹出,并赋值给局部变量i
14 return
字节码常用工具
**javap -v命令:**javap -v是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内
容。
jclasslib 的idea插件
Arthas工具
这个工具和之前的工具的不同是之前的工具无法在代码的执行过程中查看字节码,除此之外还有以下详细内容,感兴趣的话我回头重写写一篇文章专门介绍该文件
官网:https:/arthas…aliyun…com/doc/
Arthas常用命令
dump 命令:该命令可以将指定Java文件的class 保存到指定的目录当中去,使用这种方式保存的文件和运行当中的字节码文件时一致的,例如:dump -d 存放目录 类的全限定名(例如下图中包名为arthas类名为Demo 所以这里的全限定名就是 arthas.Demo),例如:
**jad命令:**可以将已加载(正在执行中的)的类的字节码文件反编译为Java文件,通过这种方式可以看出当前执行的文件的内容到底是怎样的。使用该命令可以查看当前更新的版本是否是最新版本,例如:刚刚修复了一个bug 但是不确定此时线上是否已经更新成功,可以通过该命令对更改过的文件进行反编译,再查看以确定更新后的版本是否已经成功发布到线上环境