【JVM学习笔记】-字节码-第一章(多图,多表,实操)

本文深入探讨Java字节码的结构与解析,介绍如何使用IDEA、jclasslib等工具查看字节码文件,详细解释字节码文件的组成部分,包括魔数、版本信息、常量池、访问标志等,并通过实例分析方法和字段的字节码表示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 什么是字节码

java中通过编译, 会将我们程序员编写的文件转换成为一种 .class结尾的文件,我们称之为字节码文件.它是一种二进制文件(很明显是给计算机看的文件),是Java虚拟机中运行的文件.

1.1 如何在IDEA中查看标准的,未经过IDEA反编译过的 .calss文件

(1) javap
IDEA中在 Terminal 终端中,我们通过 javap 命令来查看某个java文件的字节码文件
图1
(2) javap -c
javap -c能够将字节码文件中助记符呈现出来,能够让我们更加详细的看到字节码文件的原貌
图2
(3) javap -verbose
使用 javap -verbose命令分析一个字节码文件时, 将会分析该字节码文件的魔数,版本号,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息;
图3

1.2 Java字节码整体结构

字节数名称含义
4个字节Magic Number魔数,值为0xCAFEBABE, 是Java的创始人 James Gosling 制定
2+2个字节Version包括minor_version和major_version; minor_version:1.1(45),1.2(46)…1.8(52),指令多年不变,但是版本号每次都发生变化
2+n个字节Constant Pool包括字符串常量和数值常量等
2个字节This Class Name当前类的名字
2个字节Super Class Name父类的名字
2+n个字节Interfaces接口相关的信息(2部分组成:前两个字节表示有几个接口,后n个字节表示具体的每一个接口)
2+n个字节Fields当前类的成员变量信息,组成同上
2+n个字节Methods当前类的方法的信息
2+n个字节Attributes当前类的附加的属性

普及两个概念

  • 字节数据直接量

这是基本的数据类型, 共细分为 u1, u2, u4, u8四种, 分别代表连续的1个字节,2个字节,4个字节,8个字节组成的整体数据;

  • 表(数组)

表是由多个基本数据或其他表,按照既定顺序组成的大数据集合. 表是有结构的, 它的结构体现在: 组成表的分析所在的位置和顺序都是已经严格定义好的;

1.3 通过工具WinHex打开字节码文件,查看16进制文件内容

Test1.class的16进制文件内容

1.4 Java字节码结构解读

  • 魔数

所有的 .class字节码文件的前4个字节都是魔数, 魔数值为固定值:0xCAFEBABE(cafe babe)

  • 版本信息

魔数之后的4个字节为版本信息, 前两个字节表示 minor version(次版本号), 后两个字节表示 major_version(主版本号). 这里 1.3中4,5,6,7 四个字节表示版本号,为 00 00 00 34,换算成10进制,表示次版本号(0000) 为0, 主版本号(0034)为52(52对应java版本为1.8),所以该文件的Java版本号为:1.8.0(这个在1.2的javap -verbose编译的信息中可以看到)

  • 常量池(Constant Pool)

紧接着主版本号之后的就是常量池入口. 一个Java类中定义的很多信息都是由常量池来维护和描述的. 可以将常量池看做是 class文件的资源仓库. 比如说 Java类中定义的方法与变量信息. 都是存储在常量池中. 常量池中主要存储两类常量: 字面量符号引用. 字面量如文本字符串, java中声明为final的常量值等, 而符号引用如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符等.

每个常量其结构为, 1(个字节) + n(个字节), 表示 常量类型 + 具体常量内容
class字节码文件的 16进制文件中 2位 1字节
常量类型根据下图Class文件结构中常量池中11中数据类型的结构总表来确定, 每个数据类型的结构都有明确标出

在这里插入图片描述

  • 常量池的总体结构

Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成. 常量池数量紧跟在主版本号后面,占据2个字节; 常量池数组则紧跟在常量池数量之后. 常量池数组与一般的数组不同的是, 常量池数组中不同的元素的类型,结构都是不同的,长度当然也就是不同; 但是, 每一种元素的第一个数据都是一个 u1 类型,该字节是个标志位,兼具一个字节. JVM在解析常量池时, 会根据这个 u1 类型来获取元素的具体类型. 指的注意的是,常量池数组中元素的个数=常量池数-1(其中0暂时不使用),目的是满足某些常量池引值的数据在特定情况下需要表达"不引用任何一个常量池"的含义;根本原因就在于索引为0 也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值,所以常量池的索引从1而非0开始;(1.2图示中从#1开始就是这么个意思)

举个栗子吧,分析一下常量池的第一个常量
具体的字节码内容我们在 1.3通过 winHex工具打开,我们 只分析第一个常量,后面的都是一样的
在这里插入图片描述
上图我们分别标记了字节码表的前15个字节,共分成4各部分, 分别表示为 魔数 主次版本号 常量池数量 第一个常量池信息(这个我是根据上图 “Class文件结构中常量池中11中数据类型的结构总表” 得到的),因此我们只需要分析字节码0A 0004 0015
根据上面所述,第一个字节0A(换算成10进制为10),是该常量在 “常量池中11中数据类型的结构总表” 对应的一个类型, 为CONSTANT_Methodref_info,其包含三个部分,如下表,我们直接将解读内容也列举出来

名称对应字节码解读
tag(u1即占一个字节)0A这个值需要去结构总表中找,即我们已经找到的CONSTANT_Methodref_info
index(u2)具体含义看"结构总表"0004对应常量池内容 java/lang/Object
index(u2)0015(换算成10进制为 21)对应常量池内容 <init> ()V

表中说的常量池, 就是我们在 1.2中通过javap -verbose编译字节码文件,得到的内容中的 Constant pool里面的数据

解读第一个常量,就是Object类的构造方法. 因为<init>的意思就是构造函数而()V意思是不接受任何参数,无返回值; 结合一下,就是无参构造方法

其它常量池常量如上炮制即可

  • 描述信息

在 JVM 规范中,每个变量/字段都有描述信息. 描述信息主要的作用是描述字段的数据类型,方法参数列表(包括数量,类型与顺序)与返回值. 根据描述符规则, 基本数据类型和代表无返回值的 void 类型都用一个大写字符 V 来表示, 对象类型则使用字符 L加对象的全限定名来表示. 为了压缩字节码文件的体积,对于基本数据类型, JVM都只使用一个大写字母来表示, 如下所示 👇

B - byte, C - char, D - duble, F - float, I - int, J - long, S - short, Z - boolean, V - void, L - 对象类型,如 Ljava/lang/String

  • 数组类型表示方式

对于数组类型来说,每一个维度使用一个前置的 [来表示,如 int[]被记录为 [I, String[][]被记录为[[Ljava/lang/String

  • 描述符描述方法

用描述符描述方法时, 按照先参数列表, 后返回值的顺序来描述, 参数列表按照参数的严格顺序放在 一组()之内,如方法:String getRealnamebyIdAndNickname(int id, String name)的描述符为:(I,Ljava/lang/String;)Ljava/lang/String

  • Access_Flag 访问标志

访问标志信息包括该 Class文件是类还是接口,是否被定义为public,是否是abstract,如果是类,是否声明成final

下图为JVM规范预定义的attribute
访问标志类型

访问标志紧跟常量池后面, 有两个字节,

Test1的16进制文件中访问标志的两个字节

我们可以看到访问标志两个字节是0021,可是根据图片 Table 4.1. Class access and property modifiers 我们判断 Test1 的访问标志应该为 0x0001因为我们的类是public类型的,那为什么是0021呢,原因其实很简单 👇
因为:0x0021是0x0020和0x0001的并集,表示 ACC_PUBLIC与ACC_SUPER

  • 当前类的名字(this class name)

占两个字节
跟在 Access_Flag后面的就是 this class name 我们可以看到数值是0003,这个时候我们根据javap -verbose编译结果去看常量池#3的值,发现它引用了#23(看#3的注释就很清晰了),很明显#23的值为该类的名称

  • 父类的名称(super class name)

占两个字节
跟在 this class name后面,值为0004,具体解读方式跟this class name一样

  • interfaces 相关接口信息

interfaces由两部分组成(2+n个字节), 前两个字节表示接口的个数,我们看到值为0000,就说明没有接口实现,后面的接口描述部分则不存在;
后续补充一个实现了接口的字节码

  • 当前类的成员变量信息

2+n个字节, 由两部分组成
fields字段表示用于描述类和接口中声明的变量. 这里的字段包含了类别变量以及实例变量, 但是不包括方法内部声明的局部变量.
前两个字节表示字段的个数, 0001,表示当前类有一个字段;
后面的部分比较复杂,单独分析如下👇

第二部分(字段表(字段数组)),每一个字段都有自己的一个组成信息,包括以下

类型名称数量
u2(表示两个字节)access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

根据上表我们来结合16进制文件和javap -verbose信息得到如下结果

名称字节对应verbose信息
access_flags0002private
name_index0005a
descriptor_index0006I
attributes_count0000说明没有属性
attributes就不会出现

以上表格内容,解读下来就是,
com.turnsole.myjvm.mycalss.Test1类中的名称为 aint类型的变量

1.5 字节码解读之 Methods

注: 上面Fields 分析是在16进制文件 00000288行的 00060000结束的

因为 Methods 内容较多,我们单独拿出来做分析

方法

方法和变量一样也是由两部分组成, 头两个字节表示的是当前类的方法的个数(字节码为0003,表示总计有三个方法), 后面是方法的 方法表(方法数组),有每一个方法的具体信息, method_info

我们看下 method 在字节码中具体的结构

类型名称数量
u2access_flags1
u2name_index(名字的索引,指向常量池)1
u2descriptor_index(描述的索引,指向常量池)1
u2attributes_count(属性个数)1
attribute_infoattributesattributes_count

根据上句表格,结合ACCESS_FLAGS图,常量池数据图,我们来简单理解一下method在在字节码中的内容

名称字节数值对应数值
access_flags0001public
name_index0007<init>(注意只要是init,就说明这个方法是构造方法)
descriptor_index0008()V(表示不需要任何参数,并且无返回值)
attributes_count0001属性个数总计一个
attribute_info下面解读👇

attribute_info的结构如下:

attribute_info {
	u2 attribute_name_index;
	u4 attribute_length;
	u1 info[attribute_length];
}

方法的每个属性都是一个attribute_info结构

名称字节数值对应数值
attribute_name_index 属性名索引(对应常量池)0009Code
attribute_length0000 002F(换算成10进制,值为47)表示后面47个字节为该方法的具体执行代码,即该属性的值(下图中选中的字节)
info0001

在这里插入图片描述

JVM 预定义了部分 attribute, 但是编译器自己也可以实现自己的 attribute 写入 class 文件里,供运行时使用;

不同的 attribute 通过 attribute_name_index 来区分

1.5.1 说一下Code

Code attribute 的作用是保存该方法的结构,下图为所对应的字节码构成

Code_attribute {
	u2 attribute_name_index;
	u4 attribute_length;
	u2 max_stack;
	u2 max_locals;
	u4 code_length;
	u1 code[code_length];
	u2 exception_table_length; 
	{
		u2 start_pc;
		u2 end_pc;
		u2 handler_pc;
		u2 catch_type;	
	} exception_table[exception_table_length];
	u2 attribute_count;
	attribute_info {
		lineNumberTable_attribute {
			u2 attribute_name_index;
			u4 attribute_length;
			u2 line_number_table_length;
			{
				u2 start_pc;
				u2 line_number;
			}line_number_table[line_number_table_length];
		}
	};
}
  • attribute_length

表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段,即不包含上图中的00090000002F,也就是我们截取的字节码文件中蓝色选中的部分的总长度

  • max_stack

表示这个方法运行的任何时刻所能达到的操作数栈的最大深度, 占2个字节

示例 值为0001

  • max_locals

表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量,占2个字节

示例 值为0001

  • code_length

表示该方法所包含的字节码的字节数以及具体的指令码,具体字节码即是该方法被调用时,虚拟机所执行的字节码,占4个字节

示例 值为00000005换算为十进制值也是5,也就说后面的5个字节表示当前方法的具体执行内容,每个字节都有与之匹配的助记符, 我们借助 下面的 jclasslib 工具 将会更好做分析

  • exception_table

这里存放的是处理异常的信息; 每个exception_table 都是由 start_pc, end_pc, handler_pc, catch_type 组成

  • start_pc 和 end_pc

表示所在数组中的从 start_pc 到 end_pc 处(包含 start_pc, 不包含 end_pc) 的指令抛出的异常会由这个表项来处理

  • handler_pc

表示处理异常的代码的开始处

  • catch_type

表示会被处理的异常类型, 它指向常量池里的一个异常类. 当catch_type为0时, 表示处理所有的异常

具体分析方式是一样的,这里就不再挨个分析了

1.6 jclasslib工具 验证以上分析结果是否正确

jclasslib 工具, IDEA中已经做了集成,我们只需要把该插件下载下来就好
jclasslib

重启IDEA之后,如下图
在这里插入图片描述
是不是很清晰的展示出了字节码文件的全部信息呢. 6到飞起~~~

1.7 结合工具 jclasslib 分析一个 method

下面我们结合前面学习的东西以及工具 jclasslib 来分析 方法表(methods)中 第一个 method

我们在1.5 说一下code 中, 已经找到了这第一个 method 所对应的字节码 👇
在这里插入图片描述
2A B7 00 01 B1,结合我们的工具,得到的可视化信息或者 javap -verbose中的信息
在这里插入图片描述
在这里插入图片描述
以上第一张图为工具可视化图, 第二张为javap -verbose编译所得,结果都是一样的,但是使用工具我们可以快速定位到, Oracle官网中对助记符的文档详情中. 以下我们就对我们要解读的第一个 method 中所使用的 aload_0,invokespecial,return三个助记符做一个学习.

注意,如果通过工具快速链接打不开的话,不妨使用这个链接,然后自己找~~~
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

如果阅读英文文档比较吃力,如我, 那可以参考下面的链接
https://www.cnblogs.com/cwane/p/6097838.html

  • aload_0

对应十六进制数为 2A,表示 把局部变量第一个引用型局部变量推到操作数栈

  • invokespecial

对应十六进制数为B7,表示 调用父类构造方法,实例初始化方法,私有方法
B7后面两个字节0001对应常量池#1 = Methodref在截图中就能看出// java/lang/Object."<init>":()V,表示当前类的父类构造器的内容

  • return

对应十六进制数为B1,表示 从当前方法返回voiid

继续往下分析
紧跟在code后面的是exception_table_length,占据 2个字节, 示例值为 0000,也就是没有内容
再下面就是 attribute_count, 占据2个字节, 示例值为0002, 表示该方法有两个属性(切记莫和类的成员变量混淆,这里的属性是编译器自动生成的)

继续查看class字节码后面两个字节000A,对应常量池中#10,值为LineNumberTable(表示字节码行号与源代码行号的关系),这是第一个属性的名字

  • LineNumberTable
LineNumberTable_attribute {
	u2 attribute_name_index;
	u4 attribute_length;
	u2 line_number_table_length;
	{
		u2 start_pc;
		u2 line_number;
	}line_number_table[line_number_table_length];
}

直接上分析表

属性名字节值解读
attribute_name_index(占2字节),值在常量池中000ALineNumberTable
attribute_length(占4个字节)00 00 00 06意思就是这个属性占字节码中的6位字节,也就是说后面6个字节就是这个属性的内容
line_number_table_length(占2个字节)表示当前属性有几对映射0001意思就是有一对映射(每个映射占2个字节)
start_pc(占2个字节) 字节码偏移量0000偏移量为0
line_number(占2个字节) 源代码行号000D行号为13

jclasslib工具信息验证一下
在这里插入图片描述
源代码
在这里插入图片描述

JVM会在上图中圈红的部分 生成 这一个属性值,提前剧透 是 this

继续往下分析
LineNumberTable结束之后, 后面两个字节000B就是我们要分析的第二个属性,在常量池中的值为LocalVariableTable(局部变量表)

  • LocalVariableTable

局部变量表的结构 与 行号表 的结构差不多

首先
前4个字节0000000C,表示当前属性值所占字节码的长度,值为10进制的12
后面12个字节码为👇
在这里插入图片描述
然後
前2个字节0001,表示局部变量的个数,此处为1
再后
后面4个字节,00000005,就表示局部变量的开始位置和结束位置,此处值为0和5
然后
后面一个字节,00表示局部变量的索引, 此处值为0
再然后
后面一个字节0C表示该局部变量对应常量池中的值,对应常量池值为#12,即值为this,即当前对象
再再然后
后面2个字节000D,表示对该局部变量的描述,值也是在常量池中寻找,#13,值为Lcom/turnsole/myjvm/mycalss/Test1
意思就是 this表示上面这个对象
最后
最后两个字节0000,主要是用来做校验检查的,这里没找到具体的资料…

思考:this这个局部变量为什么会在无参构造函数中呢?

从字节码角度来说,如果类中的方法是非静态方法,编译器会将this作为第一个参数给隐式的传进来

1.8 最后一部分,整个字节码文件的attrubite信息

在这里插入图片描述
直接上分析表

类型字节码值解读
attribute_count(占2个字节)属性个数0001一共有1个属性
attribute_name(占两个字节),常量池中寻找对应值0013#19 SourceFile (源文件)
attribute_length(占4个字节),属性长度,即后面多少字节码代表该属性信息000002后面2个字节表示该属性信息
attribute_dec(n个字节)属性描述信息0014常量池中对应 #20 Test1.java(源文件名称)

以上
END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值