前言
背景
-
最近,笔者在研究源码时,突然对源码的底层实现产生了好奇。
-
代码在底层的世界到底是怎么样的呢?
-
为了揭盖它神秘的面纱,笔者踏上了浪漫的求学之路,并写下此文,以作记录。
温馨提醒
阅读本文,您至少需要有一定的 Java 语法基础
图解文本文件与二进制文件
温馨提醒:如果您有字符编码和二进制文件相关的计算机知识储备,或者如果您对这部分内容不感兴趣,可以选择跳过
文章推荐:https://www.cnblogs.com/alionxd/articles/3151019.html
具体内容:如下所示
(1) 三层模型
- 首先,我们引入一个模型,方便之后的讲解
- 如下图所示,这个模型分三个层次,分别是"显示层"、“处理层”、“存储层”
- “存储层”:表示计算机在底层存储的原始数据(用0和1表示)
- “处理层”:表示原始数据被进行处理的过程
- “显示层”:表示原始数据被处理后的结果
(2) 数值的多种表示
- 接下来,我们需要认识一个概念:同一个数值的不同表示
- 对于某一个数值,在意义层面上只有一个,但是在表示层面上,可以有多种表达方式
- 换个角度思考,这是不是也意味着,对于一个给定的数字符号,我们也许不能确定它的数值呢?
- 比如,别人写了"11" 这个符号,你能确定它是数值多少吗?
- 答案是否定的,严格来说,我们不能直接确定它是多少,因为我们并不知道它是采用什么进制
- 如果它是二进制的,那么"10"则表示数值3
- 如果它是八进制的,那么"10"则表示数值9
- 如果它是十进制的,那么"10"则表示数值11
- 如果它是十六进制的,那么"10"则表示数值17
- 最后,我们能够理解一点就好:对于某一个数值来说,即使他有不同的形式,但是所代表的含义只有一个,那就是数值本身
(3) 表示二进制数据
- 我们知道,计算机底层存储的数据都是二进制的形式,也就是存储
1010 1110
而不是存储256
、174
、AE
这些符号 - 从严谨性来说,我们在讨论和描述计算机底层的数据时,就应该使用
1010 1110
- 但是,在人类世界中,我们总是直接描述
1010 1110
是很累的 - 为了方便沟通和显示数据,我们往往使用
1010 1110
的"别名"——十六进制AE
去表示 - 这就好比小明不会叫他的妻子为"古丽热巴·买买提",而是直接叫"老婆",反正本质都是指向同一个意思
- 因此,以后我们在表达二进制数据时,不再使用二进制符号表示,而是使用十六进制符号表示,你只要不忘记它本质是0101的数据即可
- 在充分理解了为什么我们用十六进制的
AE
去表示二进制数据1010 1110
后,我们将模型表示为下图
(4) 文本文件
- 所有文件,都可大致分为两类。一类是文本文件,另一类则是二进制文件
- 文本文件,通常用记事本等文本编辑器打开。在打开之后,里面显示的信息我们可以直接看懂
- 我们知道,在计算机底层文件都是以0和1进行存储的
- 要想将存储层中的二进制数据变成我们能看懂的符号,就需要对二进制数据进行处理,而这个处理的过程,我们称之为字符编码
- 常见的编码规则有 ASCII、UTF-8等
- 注意:下图中的二进制数据是随意乱写的,只是为了描述这个过程
- 由上图不难发现,为什么当你用"ASCII 编码规则"写数据到计算机中后,再将这些数据用"UTF-8 规则"读取出来会出现乱码的现象
- 文本文件的原理大致就是这样。对于其中各式各样的具体的编码规则,如果你感兴趣的话,可以自行深入了解
(5) 二进制文件
- 接下来我们讨论二进制文件
- 打个比喻,如果说文本文件是偏向用来展示的,花拳绣腿做表面功夫的,那么二进制文件,则是偏向实打实干活的,简单粗暴,干就完了!
- 二进制文件是有数据类型的,不同类型的数据类型占的字节大小不太一样
- 二进制文件通常用于保存数值,至于这些数值是什么含义就看它的设计者所给的定义了,当然,二进制文件也能保存字符型的数据(也就是和文本文件效果一样)
- 另外,大端模式和小端模式是针对数据类型的存储顺序的讨论,而不是针对字符串而言的,字符串没有所谓的大小端模式,只是按照固定顺序进行解析
- 下面,列举一个例子来帮助我们理解
- 从上图可以知道,为什么我们在使用文本编辑器工具打开二进制文件时,部分数据出现乱码现象,而部分数据没有乱码
- 同时我们也能看到,使用二进制文件存储数值比较节约空间
- 一般来说,二进制文件里面存储的数值是给计算机看的,是让计算机进行运算的,又或者这些数值有其他含义和作用。虽然这些二进制数据很原始、很简单粗暴,但这恰恰符合了它的特点:这些数据本来就不是为了给我们看的;而是给某些程序和代码的,给计算机处理的。所以,我们通常都不会用文本编辑器去打开二进制文件
- 综上所述,在面对一个二进制文件时,我们不能准确地知道它的含义,这很正常。如果真的好奇里面的数据表达什么含义或执行什么功能,我们就需要获得这些数据储存方式的说明书。说明书会告诉我们从第几个字节到第几个字节是什么类型的数据,储存的数据是什么含义。
JVM 与 Class 文件
- 代码编写完成后,需要转换成计算机 CPU 可以理解和执行的二进制机器码。这是因为 CPU 只能直接执行以 0 和 1 表示的二进制指令
- 但是,对于像 Java 这样的编程语言,它们并不是直接编译成特定 CPU 的机器码,而是编译成一种中间格式的文件,即 Class 文件(字节码文件)
- 这种 Class 文件是一种平台无关的格式,它可以在任何安装了相应虚拟机的操作系统上运行。虚拟机(如 Java 虚拟机 JVM)负责将 Class 文件中的指令转换成特定操作系统的机器码
- 虚拟机的作用是屏蔽不同操作系统和 CPU 指令集的差异,使得 Java 等语言编写的程序可以在不同的硬件和操作系统上运行,而无需为每种平台单独编译。这就是所谓的“一次编写,到处运行”
- 此外,Java 虚拟机的设计者在设计之初就考虑并实现了其它语言在 Java 虚拟机上运行的可能性
- 所以并不是只有 Java 语言能够跑在 Java 虚拟机上,时至今日诸如 Kotlin、Groovy、Jython、JRuby 等一大批 JVM 语言都能够在 Java 虚拟机上运行
- 它们和 Java 语言一样都会被编译器编译成字节码文件,然后由虚拟机来执行
Class 文件结构
(1) 整体认识
- 我们通过一个例子来开始
- 这里有一个
Student.java
的文件,里面的内容如下图所示
- 将
Student.java
文件进行编译,生成Student.class
文件。然后,我们通过二进制文件查看工具打开它
- 我们可以看到,Class 文件由一堆的二进制数据组成,以1字节为基础单位
- 现在这一堆0和1看起来很底层,查看数据起来不太方便,我们现在将这些二进制数据用十六进制的符号表示,如下图
- 好了,这下看起来舒服多了~~,我们继续…
- 可以看到,Class 文件里面的数据严格按照顺序紧凑的排列,中间无任何分隔符,这使得整个 Class 文件中存储的内容几乎全部都是程序运行的必要数据,极大的精简了空间
- 那么,这些二进制数据到底表示什么意思呢?
- 首先,对于这些二进制数据,JVM 当然非常明白它是什么意思,但我们普通人就不知道了
- 所以接下来的目的,就是让我们一起认识这些二进制数据,看看它们表达了什么意思
- 我们还是先通过图片,去直观地认识一下吧
- Class 文件内容的含义,严格按照顺序进行解析,包含以下几点
- 魔数
- 版本号
- 常量池
- 访问标记
- 索引(类索引、父类索引、接口索引)
- 字段表集合
- 方法表集合
- 接下来,我们将对这些内容逐个进行解析
(2) 具体解析
魔数
- 如图所示,红色方框所圈住的四个字节就是魔数
- 当 JVM 扫描这四个字节后,它就明白当前文件是 class 文件
- 具体而言,JVM 会在验证阶段检查文件开头是否以该魔数开头,如果不是,则会抛出
ClassFormatError
错误 - 不知道你发现没?这四个字节用十六进制表示的话,是
CA FE BA BE
,恰好与英文单词cafe babe
(咖啡宝贝) 长的一模一样。我们回想 Java 的图标,它是一杯热气腾腾的咖啡,由此可见,里面真的妙不可言啊,咖啡!好像也挺不错呢嘻嘻~
版本号
- 紧跟着魔数后面的四个字节表示版本号,其中前面两个字节表示副版本号,后面两个字节表示主版本号
- 十六进制
00 00
转换成十进制是 0,十六进制00 34
转换成十进制是 52 - 所以我们可以知道,当前这个class文件采用的 Java 技术是版本:52.0(主版本号.副版本号)
- 我们到网上一查,很轻易就能知道,字节码的版本号为52.0的,对应的就是 Java 8
- 如果 Java 7 版本的 JVM 读取当前 class 文件,当它读到版本号信息时,就会抛出异常,JVM 无法继续加载该class文件(原因:低版本的 JVM 不能理解 class 文件里面所出现的高级语法,因此无法执行)
- 如果是 Java 8 及以上版本的 JVM 读取当前 class 文件,则没有什么问题
常量池
- 紧跟在版本号之后的是常量池(红色方框所圈住的部分)
- 常量池最开始的两个字节
00 18
的十进制是 24,表示当前常量池里面有24 - 1 = 23 个常量表;也就是说,第0个常量表我们不用管(也管不着),我们是从第1个常量表开始,一直到第23个常量表,对应的是索引1~23 - 所以当我们看到常量池最开始的两个字节为
00 18
也就是十进制 24 的时候,我们心里就要清楚,接下来的一连串二进制数据对应的是23个常量表
-
我们可以看到:常量池,本质上就是一堆的二进制数据;而常量表,本质上就是其中连续的一小段二进制数据
-
常量池就相当于一个用来存放资源的大型仓库,里面存放着一个又一个的常量表;而常量表,就相当于一个小仓库,里面的东西就是真正的数据。可以说,常量表是常量池读取有效信息的基本单位
-
细心的你应该发现了,一个个常量表所占的字节数是有可能不一样的。既然如此?那到底我该如何判断哪些字节组合才是一个常量表呢?常量表里面的数据又表示着什么信息呢?
-
带着这些疑问,我们一起去认识一下常量表吧
-
常量表一共有11种,这些表的区分方法是:表结构的第一位都是类型为
u1
称为tag
的值,根据tag
值的不同来区分当前是哪一种表;对应到二进制数据中就是,每个表的第一个字节表示的是该表的种类 -
下表列出的是11种常量表的简单说明(简单浏览一下就可以了,以后需要用的时候再来这里查表)
tag 值(大白话:种类) | 描述 | 类型 | 所属类别 |
---|---|---|---|
1 | UTF-8编码的字符串 | CONSTANT_Utf8_info | 字面量 |
3 | 整型字面量 | CONSTANT_Integer_info | 字面量 |
4 | 浮点型字面量 | CONSTANT_Float_info | 字面量 |
5 | 长整型字面量 | CONSTANT_Long_info | 字面量 |
6 | 双精度浮点型字面量 | CONSTANT_Double_info | 字面量 |
7 | 类或接口的符号引用 | CONSTANT_Class_info | 符号引用 |
8 | 字符串类型字面量 | CONSTANT_String_info | 字面量 |
9 | 字段的符号引用 | CONSTANT_Fieldref_info | 符号引用 |
10 | 类中方法的符号引用 | CONSTANT_Methodref_info | 符号引用 |
11 | 接口中方法的符号引用 | CONSTANT_InterfaceMethodref_info | 符号引用 |
12 | 字段或方法的部分符号引用 | CONSTANT_NameAndType_info | 符号引用 |
- 下表展示的是11种常量表的具体结构(简单浏览一下就可以了,以后需要用的时候再来这里查表)
- 补充知识:
u1
、u2
、u4
、u8
分别表示(占1个字节的)无符号数
、(占2个字节的)无符号数
、(占4个字节的)无符号数
、(占8个字节的)无符号数
- CONSTANT_Utf8_info
类型 | u1 | u2 | u1 |
---|---|---|---|
项目 | tag | length | bytes |
说明 | 1 | UTF-8编码的字符串总共占用的字节数 | UTF-8编码的字符码值 |
- CONSTANT_Integer_info
类型 | u1 | u4 |
---|---|---|
项目 | tag | bytes |
说明 | 3 | 整型常量值 |
- CONSTANT_Float_info
类型 | u1 | u4 |
---|---|---|
项目 | tag | bytes |
说明 | 4 | 单精度浮点型常量值 |
- CONSTANT_Long_info
类型 | u1 | u4 | u4 |
---|---|---|---|
项目 | tag | high_bytes | low_bytes |
说明 | 5 | 长整型的高四位值 | 长整型的低四位值 |
- CONSTANT_Double_info
类型 | u1 | u4 | u4 |
---|---|---|---|
项目 | tag | high_bytes | low_bytes |
说明 | 6 | 双精度浮点的高四位值 | 双精度浮点的低四位值 |
- CONSTANT_Class_info
类型 | u1 | u2 |
---|---|---|
项目 | tag | name_index |
说明 | 7 | 存储constant_pool中的索引值,CONSTANT_Utf8_info类型 |
- CONSTANT_String_info
类型 | u1 | u2 |
---|---|---|
项目 | tag | string_index |
说明 | 8 | 存储constant_pool中的索引值,CONSTANT_Utf8_info类型 |
- CONSTANT_Fieldref_info
类型 | u1 | u2 | u2 |
---|---|---|---|
项目 | tag | class_index | name_and_type_index |
说明 | 9 | 存储constant_pool中的索引值,CONSTANT_Class_info类型。记录定义该字段的类或接口 | constant_pool中的索引值,CONSTANT_NameAndType_info类型。指定类或接口中的字段名(name)和字段描述符(descriptor) |
- CONSTANT_Methodref_info
类型 | u1 | u2 | u2 |
---|---|---|---|
项目 | tag | class_index | name_and_type_index |
说明 | 10 | 存储constant_pool中的索引值,CONSTANT_Class_info类型。记录定义该字段的类或接口 | constant_pool中的索引值,CONSTANT_NameAndType_info类型。指定类或接口中的字段名(name)和字段描述符(descriptor) |
- CONSTANT_InterfaceMethodref_info
类型 | u1 | u2 | u2 |
---|---|---|---|
项目 | tag | class_index | name_and_type_index |
说明 | 11 | 存储constant_pool中的索引值,CONSTANT_Class_info类型。记录定义该字段的类或接口 | constant_pool中的索引值,CONSTANT_NameAndType_info类型。指定类或接口中的字段名(name)和字段描述符(descriptor) |
- CONSTANT_NameAndType_info
类型 | u1 | u2 | u2 |
---|---|---|---|
项目 | tag | name_index | name_index |
说明 | 12 | constant_pool中的索引,CONSTANT_Utf8_info类型。指定字段或方法的名称 | constant_pool中的索引,CONSTANT_Utf8_info类型。指定字段或方法的描述符 |
- 通过简单浏览上面的表,我们对常量表的种类和结构应该有了初步的认知和了解。为了进一步认识,我们继续上面的例子
- 常量池最开始的两个字节
00 18
十进制是 24,表示当前常量池有有23个常量表,紧接着出现的字节是0A
,它的十进制是 10,表示tag=10,我们去查 tag 为 10 的表,发现它是CONSTANT_Methodref_info
表,它的结构是[u1、u2、u2]
(1字节、2字节、2字节),结合表的具体信息,我们不难得出下面的结论 0A 00 02 00 03
整体表示的是CONSTANT_Methodref_info
这个常量表- 其中的
0A
的十进制是 10,表示它是表的类型是 tag=10 - 而
00 02
的十进制是 2,表示类或接口为#2
(#2的意思是:让我们转去常量池的第2个常量表里面找具体数据) - 至于
00 03
,它的十进制是 3,表示类或接口中的字段名和字段描述符为#3
(#3的意思是:让我们转去常量池的第3个常量表里面找具体数据)
-
接下来,我们将一口气解析好几个常量表,以帮助我们真正快速的理解它的含义
-
为了更方便的解释其中含义,对于这些二进制数据,我们将不再采用十六进制进行表示,我们现在采用十进制进行表示
十六进制表示法
十进制表示法
- 解析结果如下图所示
- 在常量池里面,以
001
开头,并且后面紧跟着2个字节表示字符串占用的字节个数的,其后面的二进制数据是字符串信息,它们采用UTF-8进行编码
- 为了更方便的查看里面的数据,这里我们使用一款 IDEA 的插件——
jclasslib
- 将源代码进行编译之后,会生成 .class 文件,我们用
jclasslib
插件打开它,它会自动解析文件里面的二进制数据并以可读的方式展示给我们看
- jclasslib 解析出来的内容,和我们讲述的完全一致,是不是感觉非常方便?
- 至此,我们已经对常量池有着较为深刻的认识了
访问标记
- 在常量池结束之后,紧接着的两个字节代表访问标记(access_flag)
- 这个标记用于识别一些类或者接口层次的访问信息,包括:这个
.class
文件是类还是接口,它是否定义为 public 类型,它是否定义为 abstract 类型;如果它是类的话,有没有声明 final 等等 - 常用的标记如下表所示
标记值 (0x00) | 标记名称 | 描述 |
---|---|---|
00 01 | ACC_PUBLIC | public 类型 |
00 10 | ACC_FINAL | final 类型 |
00 20 | ACC_SUPER | 调用父类的方法时,使用 invokespecial 指令 |
02 00 | ACC_INTERFACE | 接口类型 |
04 00 | ACC_ABSTRACT | 抽象类型 |
10 00 | ACC_SYNTHETIC | 标记为编译器自动生成的类 |
20 00 | ACC_ANNOTATION | 标记为注解类 |
40 00 | ACC_ENUM | 标记为枚举类 |
80 00 | ACC_MODULE | 标记为模块类 |
- 注意标记是可以进行组合的,比如
public
和final
组合的标记值是00 11
- 本例中是
00 21
,也就是使用了 public00 01
和默认的 (super)00 20
索引
- 访问标记之后紧接着的是索引,包含三个部分:类索引、父类索引、接口索引。这三部分用来确定类的继承关系
- 下图中的
00 07
是类索引,00 02
是父类索引,00 00
是接口索引(00 00
表示这个.class
文件没有接口索引)
字段表集合
- 在索引之后,就到
字段表集合
字段表集合
格式为:字段表的总个数
+字段表
+字段表
+...
- 其中
字段表
格式为:字段访问标志
+字段名索引
+字段描述符索引
+属性表集合
- 而
属性表集合
格式为:属性表的总个数
+属性表
+属性表
+...
字段表的结构
类型 | u2 | u2 | u2 | ~ |
---|---|---|---|---|
项目 | access_flag | name_index | descriptor_index | ~ |
描述 | 字段访问标记 | 字段名索引 | 字段类型(描述符)索引 | 属性表集合 |
字段访问标记
标记值 (0x00) | 标记名称 | 含义 |
---|---|---|
00 01 | ACC_PUBLIC | 字段为public |
00 02 | ACC_PRIVATE | 字段为private |
00 04 | ACC_PROTECTED | 字段为protected |
00 08 | ACC_STATIC | 字段为static |
00 10 | ACC_FINAL | 字段为final |
00 40 | ACC_VOLATILE | 字段为volatile |
00 80 | ACC_TRANSIENT | 字段为transient |
10 00 | ACC_SYNCHETIC | 字段为synchetic |
40 00 | ACC_ENUM | 字段为enum |
属性表的通用结构
对于每一个属性表,它的属性名称都要从常量池中引入一个 CONSTANT_UTF8_info
类型的常量来表示,而属性表后半部分的结构,则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的字节数
类型 | u2 | u4 | ~ |
---|---|---|---|
项目 | attribute_name_index | attribute_length | info |
描述 | 属性名称的索引 | 属性值的长度 | 属性值 |
- 在上面,我们列出了在分析
字段表集合
过程中所有可能要用到的结构信息。接下来,我们结合这些信息,继续分析class文件的数据 - 一个类中定义的字段会被存储在字段表中,包括静态的和非静态的
- 参考上面的格式信息可知,下图最开始的两个字节
00 01
表示字段表的总个数。这里00 01
的十进制是 1,也就是总共有 1 个字段表
- 在知道了总共有 1 个字段表后,就到了分析具体的字段表了,我们先看前面 6 个字节
00 19
、00 09
、00 0A
- 在上图中,
00 19
是关于字段访问标记的信息。参考字段访问标记的表格可知,00 19
由00 01
、00 08
、00 10
组合而成,也就是该字段的访问标记是 **public static final ** - 而
00 09
表示该字段名索引。00 09
十进制是 9,说明该字段名称的索引为 9,即:要想找该字段的名称,就需要去常量池第 9 个常量表 (#9) 里面找 00 0A
的十进制是 10,表示该字段类型(描述符) 在 #10 的位置。即:要想知道该字段的类型,就需要去常量池的第 10 个常量表里面找- 我们可以借助 jclasslib 插件验证上述说法是否正确
-
由格式
字段访问标志
+字段名索引
+字段描述符索引
+属性表集合
可知,接下来我们分析的是:该字段的属性表集合 -
属性表集合
=属性表的总个数
+属性表
+属性表
+...
-
由下图可知
00 01
表示该字段拥有的属性表个数。由于00 01
的十进制是 1,所有该字段只有 1 个属性表
- 紧接着我们分析具体的属性表
- 参考属性表的通用结构可知,下图中的
00 0B 00 00 00 02 00 0C
应该划分成00 0B
和00 00 00 02
和00 0C
00 0B
十进制是 11 ,表示要去常量池#11
找具体的属性名称,这里我们使用 jclasslib 去查看这个属性名称是什么,如下图所示
00 00 00 02
十进制是 2,表示属性值长度为 2,即:接下来的 2 个字节是属性值00 0C
的十进制是 12,表示属性值为 12,意味着要去常量池#12
查找具体的值,我们到 jclasslib 找找看,这个值最终是什么?
- 截至目前为止,我们对整个"字段表集合 "的学习就基本结束了
- 但是,这里还有一个重点内容需要补充
- 我们把注意力集中到下图中的 “描述符” 那里
- “描述符”,就是类型的意思。图片里面显示它的值为
#10
,意味着要我们去常量池的第10个常量表里面找。但是,你看旁边红色的符号,是不是显示<I>
,上面说过,这个是预执行的值,就是帮我直接把#10
位置的字符直接显示给我们看,方便我们学习,而不用大费周章自己去查找常量池里面的第10个常量表 - 我们这里是学习阶段,所以我们还是亲自去常量池里面找找看,顺便验证一下上面的说法是否正确
- 亲自查找过后,我们确认上面的说法是正确的,也就是说,字段
aaa
的描述符(类型) 是I
。类型I
是什么意思呢? - 对于基本数据类型,我们用一个字符来表示,比如说
I
对应的是int
,B
对应的是byte
- 对于引用数据类型,我们用
L***;
的格式来表示,比如说一个经典的引用数据类型——字符串,它是Ljava/lang/String;
- 对于数组来说,我们用
[
符号放在开头,比如说字符串数组是[Ljava/lang/String;
- 至此,我们完美结束字段表集合的学习
方法表
-
字段表集合之后,就是方法表集合
-
与字段表集合类似,方法表集合也是采用一样的套路
-
方法表集合
格式为:方法表的总个数
+方法表
+方法表
+...
-
其中
方法表
格式为:方法访问标志
+方法名索引
+方法描述符索引
+属性表集合
-
而
属性表集合
格式为:属性表的总个数
+属性表
+属性表
+...
方法表的结构
类型 | u2 | u2 | u2 | ~ |
---|---|---|---|---|
项目 | access_flags | name_index | descriptor_index | ~ |
描述 | 方法访问标记 | 方法名索引 | 方法类型(描述符)索引 | 属性表集合 |
方法访问标记
标记值 (0x00) | 标记名称 | 含义 |
---|---|---|
00 01 | ACC_PUBLIC | 方法为public |
00 02 | ACC_PRIVATE | 方法为private |
00 04 | ACC_PROTECTED | 方法为protected |
00 08 | ACC_STATIC | 方法为static |
00 10 | ACC_FINAL | 方法为final |
00 20 | ACC_SYNCHRONIZED | 方法为synchronized |
00 40 | ACC_BRIDGE | 方法是由编译器产生的桥接方法 |
00 80 | ACC_VARARGS | 字段接受不定参数 |
01 00 | ACC_NATIVE | 字段为native |
04 00 | ACC_ABSTRACT | 方法为abstract |
08 00 | ACC_STRICTFP | 方法为strictfp |
10 00 | ACC_SYNTHETIC | 方法是由编译器自动产生的 |
属性表的通用结构
对于每一个属性表,它的属性名称都要从常量池中引入一个 CONSTANT_UTF8_info
类型的常量来表示,而属性表后半部分的结构,则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的字节数
类型 | u2 | u4 | ~ |
---|---|---|---|
项目 | attribute_name_index | attribute_length | info |
描述 | 属性名称的索引 | 属性值的长度 | 属性值 |
-
在上面,我们列出了在分析
方法表集合
过程中所有可能要用到的结构信息。接下来,我们结合这些信息,继续分析class文件的数据 -
方法表用来存储方法的信息,包括方法的修饰符,方法名称,方法参数。
方法访问标记,用于描述方法的修饰符;
方法名索引,指向常量池中的某一项,用于描述方法的名字;
方法描述符索引,指向常量池中的某一项,用于描述方法的参数和返回值类型。
-
参考格式信息可知,下图最开始的两个字节,表示方法表的总个数
-
很明显,这里
00 02
的十进制是 2,也就是总共有 2 个方法表
- 接下来,我们进入到第 1 个方法表进行具体分析
- 还记得方法表的格式吗?是的,就是:
方法访问标志
+方法名索引
+方法描述符索引
+属性表集合
- 我们先看
方法访问标志
、方法名索引
和方法描述符索引
- 如下图,
00 01
表示方法的访问标记是 public ;而00 05
十进制是 5,表示方法名在常量池的#5
位置;对于00 06
十进制是 6,表示方法描述符(也就是方法类型)在常量池#6
位置
- 我们也可以在 jclasslib 插件里面查看信息,检验一下是否正确
- 现在,我们来看该方法的
属性表集合
- 回忆一下:
属性表集合
格式为:属性表的总个数
+属性表
+属性表
+...
- 所以,下图中的
00 01
十进制是 1,表示总共有 1 个属性表
-
查找属性表的通用格式可知
00 0D
十进制是 13,表示属性名称的索引为 13(在常量池#13
位置)00 00 00 2F
十进制是 47,表示属性值的长度为 47而橙色标记的 47 个字节数据,表示具体的属性值
- 我们通过 jclasslib 去查看这些数据到底是什么意思,如下图所示
- 很明显,属性名索引为
#13
,其指向的内容是 “Code” - 然后,这个 Code 属性的属性值长度是 47
- 具体的属性值就是"字节码"红色方框圈住的内容。这些内容,实际上就是方法体里面的源代码在经过编译器编译后,变成的字节码指令。我们通常会查看这些字节码指令,去判断源代码执行效率的高低!
- 至此,我们就分析完了第 1 个方法表
- 同理,第二个方法表也是这样进行分析,这里我们就快速分析一下吧
-
结合结构信息分析上图:
00 09
表示方法访问标记由00 01
和00 08
组合而成,也就是 public static00 12
的十进制是 18,表示方法名索引为#18
00 13
的十进制是 19,表示方法类型(描述符)索引为#19
00 01
的十进制是 1,表示方法总共有 1 个属性表00 0D
的十进制是 13,表示属性名索引为#13
00 00 00 2B
的十进制是 43,表示属性值的长度是 43 个字节橙色标记的 43 个字节数据,表示具体的属性值
-
我们到 jclasslib 插件查看详细内容
- 至此,我们就快速分析完了第 2 个方法表
- 方法表集合的学习就告一段落了
属性表集合
-
属性表集合分为三种:
第一种属性表集合,是字段表里面的属性表集合
第二种属性表集合,是方法表里面的属性表集合
前面的这两种,我们在上面都学习过
而本节要讲述的内容,就是第三种属性表集合
-
第三种属性表集合,它是紧跟着方法表集合之后的,主要的用途就是描述 class 文件所携带的一些辅助信息,例如:该 class 文件的名称等等
-
属性表集合的格式,和属性表的格式,都是通用的,这里我们就直接快速讲解了,不再详细分析
- 如上如所示,
00 01
的十进制是 1,表示属性表集合总共有 1 个属性表;00 16
的十进制是 22,表示属性名索引为#22
;00 00 00 02
的十进制是 2,表示属性值的长度占 2 个字节;00 17
表示具体的属性值(00 17
的十进制是 23) - 我们借助 jclasslib 查看里面具体内容
- 至此,我们完美结束
总结
(1) class 文件内容顺序总体上和 Java 源文件内容顺序一致
(2) class 文件绝大部分信息,是保存在常量池中
(3) 在未来,我们通常会使用 jclasslib 或类似的工具来帮助我们快速查看 class 文件内容,而不是自己去对照组 JVM 规范翻译数据内容
至此,全文结束!感谢您的观看,希望能帮助您。