同事:你能跟我聊聊class文件么?(下篇)
3.4 访问标记
等到常量池的部分结束后,紧随其后的就是访问标记了。访问标记其实很好理解,就是类上的修饰符,如final、abstract等。
这个部分用两个字节的空间来保存,其实这里十六进制值存储的也是约定好的枚举值,不同的枚举值对应不同的访问标记名:
在上面这个例子1中,访问标记就是00 21
:
00 21
对应的就是ACC_PUBLIC
以及ACC_SUPER
,意味着这个类是public修饰的类,而ACC_SUPER
则代表该类有继承关系。这也很好理解,在Java中,所有的类都是Object类的子类嘛。
3.5 类索引(this_class)
顾名思义,类索引保存的是一个与类相关的索引,既然有索引那么肯定有数据,那么它指向的数据是哪呢,就是前面提到的常量池。
在例1中,类索引紧跟访问标记00 21
之后,也就是对应的00 07
,指向常量池中索引为7的位置:
我们找到常量池中索引为7的位置,发现这个位置对应的其实是一个CONSTANT_Class_info
,代表这是一个类的信息常量:
类对应的类名是cp_info #36
,我们继续找下去,可以看到,this_class最终指向的是一个字符串字面量,也就是本类的类名:
3.6 超类索引(super_name)
与类索引的查询方法一致,在例子1中,对应的超类索引十六进制值为00 08
:
同样到常量池中取索引,得到最终的超类名为:
所有的类都是Object类的子类,在这一步得到了验证。
3.7 接口索引(interface)
与前两个索引的查询方法一致,不过这里有一些特殊点,在例子1中,由于该类没有实现任何接口,所以接口表索引为00 00
:
还记得前面所说的常量池的索引从1开始,而0号位是无效位吗?
将0号位置设为无效就是为了应对这种情况。
由于常量池中没有0号索引位,因此读取到00 00
这样的索引时可知,此索引表示的是 不存在该信息。
3.8 字段表
接口索引之后紧跟的是字段表,字段表很好理解啦,记录的就是类中的字段信息。前面已经说过,Class文件中的表的结构,都是以 表大小+表内容 来表示的,字段表当然也不例外,它的表示为:
{
u2 fields_count;
field_info fields[fields_count];
}
fields_count
表示的是fields
中的field_info
数量。
字段内容field_info
内部又可以细分为:
{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
我们来简单介绍一下这几个部分的内容:
3.8.1 access_flags
访问标记。这跟类的访问标记相似,只不过字段表中的访问标记存储的是字段上的public、private、protected等信息,当然也包括static、final等声明信息,同样是以枚举的方式记录:
3.8.2 name_index
字段名索引。跟3.5中的类索引相似,name_index
记录的是常量池中的索引,最终可以通过name_index
找到一个字符串常量名,也就是字段名。
比如,在例子1中,第一个字段的name_index
如下:
再到常量池索引为#9
的位置查找,最终得到的字段名就是a
啦。
3.8.3 descriptor_index
字段类型索引。
跟字段名索引的功能类似,指向常量池中的一个字符串常量,但是为了节省空间,字段的类型是用简写方式表示的,例如:
- 基础类型,byte、int、char、float等这些简单类型使用一个大写字符来表示,B对应byte类型,I对应的是Integer,大部分类型对应都是自己本身的首字母大写,除了有两个特殊的——J对应的是long类型,Z表示boolean类型。
- 引用类型使用L+全类名+;的方式来表示,为了防止多个连续的引用类型描述符出现混淆,引用类型描述符最后都加了一个分号";"作为结束,比如字符串类型String的描述符为
Ljava/lang/String;
。 - 数组类型用"["来表示,如字符串数组String[]的描述符为“
[Ljava/lang/String;
”,同时该符号也可表示多维数组,如int[][]
就被表示为[[I
。
在例子1中,变量a的descriptor_index
为00 0A
,也就是指向索引#10
的位置,最终找到变量a的类型为I
,也就是Integer。
3.8.4 attributes_count与attributes(属性表)
attributes_count
表示的是字段属性项个数,而attributes
则是字段属性项集合。
attributes
中由各类的attribute_info
组成,attribute_info
记录的是具体的属性信息,比较常见的有ConstantValue
属性,表示这个字段是一个常量;还有RuntimeVisibleAnnotations
属性,表示该字段上标注有运行时注解,比如Spring相关注解。
关于运行时注解与编译时注解我们在编译过程中已经讨论过了,还有疑惑的同学可以回过去复习一下。
可以发现,字段表其实是一个嵌套式的表结构,field_info
表内部嵌套一个attributes
。
我们回到Class文件中看看他们是怎么表示的,在例子1javap
结果中间的部分,在#37
之后的一行,有关于变量a
的描述:
private final int a;
descriptor: I
flags: ACC_PRIVATE, ACC_FINAL
ConstantValue: int 100
这里展示的就是变量a
对应的访问标记
、字段类型
以及属性表
的内容啦。
在jclasslib中就看得更清楚了:
这就是字段表中保存的关于变量a的相关信息了。
等等,这里有一个重点!!
我们在3.3 常量池的部分中抛出了一个问题,为什么int类型字段值只有声明为final后才会被保存到常量池中?
这里就能得到答案。
因为声明为final的字段,需要在attribute_info
中存储ConstantValue
属性来标识该字段是一个常量。而ConstantValue
又需要记录下该字段的值。
这个值保存在哪个地方最适合呢,当然是常量池啦。
我们使用jclasslib查看就更直观了,常量值的索引指向cp_info #12
,而常量池中#12
位置存储的正是值100
。
未被final声明的int类型,其值不会保存到常量池中,而是在使用时直接嵌入到字节码中,如本例子中的变量b:
3.9 方法表
方法表的作用和字段表很类似,用于记录类中定义的方法。
当然,方法表前也是有一个count记录的,具体的结构如下:
{
u2 methods_count;
method_info methods[methods_count];
}
method_info
也是一个嵌套结构:
{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
前四个部分就不用介绍了,跟字段表中的信息基本一致,描述的是一些方法声明信息。
我们直接来聊聊方法表中的这个attribute_info
,也就是方法中的属性表。
字段中的属性表存储了字段的常量值等信息,而通常来说,方法的定义是要比字段定义稍微复杂一些的,比如方法有方法体,有声明的抛出异常等,而这些信息,就都存储在方法表里的attribute_info
里。因此方法中可用的attribute_info
要更多。
例如方法体对应的属性是Code
,而异常信息对应的属性名为Exceptions
,这都是字段中不存在的属性。
在方法表的属性表中,最为重要的就是Code
属性,例如例子1中的main方法,其Code
属性在字节码中是这样表示的:
Code:
stack=1, locals=2, args_size=1
0: bipush 102
2: istore_1
3: return
LineNumberTable:
line 8: 0
line 9: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 e I
其中最重要的部分,在这:
这一部分就是我们俗称的 字节码。
开篇我们就强调了,字节码是Class文件的一部分,但它究竟在什么位置,现在就有了答案:
字节码记录在Class文件的方法表中的attribute_info的Code属性里。
这里暂时不展开讲Code部分,各位同学不要心急,后续会单独写一篇字节码相关的文章,保证给大家安排地明明白白。
至于stack(操作数栈),locals(局部变量),LineNumberTable(行号表)和LocalVariableTable(局部变量表),都是JVM运行时需要相关的信息,我们可以暂时不用纠结,后续也会接触到的。
现在大家只要记住字节码其实保存在方法表中就可以了。
3.10 属性表
是不是很眼熟?属性表在前面已经出现过了,在字段表中、方法表中,都内嵌了一个属性表。
而这里的属性表,记录的是该类的类属性(注意不是类字段),它的结构如下:
{
u2 attributes_count;
attribute_info attributes[attributes_count];
}
attribute_info
的具体结构又可细分:
{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
所以说啊,类中有一个属性表,方法中有一个属性表,字段中还是有一个属性表,但它们记录的东西不一样:
- 字段表中的属性表记录的是字段是否为常量、字段上是否有注解等信息。
- 方法表中的属性表记录了方法上是否有注解、方法的异常信息声明、字节码等信息。
同样的,不同位置的属性表可供使用的属性也不一样,比如ConstantValue
不能用在类和方法上,Exceptions
属性不能用在字段上。
那Class文件最后的这个属性表记录的跟类的相关信息具体有哪些呢?
只说几个常见的:
- SourceFile:类的源文件名称。
- RuntimeVisibleAnnotations:类上标记的注解信息。
- InnerClasses:记录类中的内部类。
属性表的规则相比于其他的部分较为松散一些,第一,属性表中的属性并没有顺序要求,第二,不同的属性内部的具体的info内容结构也是各异的,需要按照虚拟机的规范事先约定好,虚拟机读取到对应的属性名称后,再按规范去解析其中的属性信息。
我们举一个例子,在方法表的属性表中(有点像绕口令哈哈),可能会存在LineNumberTable
和Exceptions
两种属性,虽然它们的开头部分是一致的:一个占用两字节的attribute_name_index
,以及一个占用四字节的attribute_length
。
但虚拟机根据读取到attribute_name_index
进行的后续解析步骤是不同的,比如对于Exceptions
,后续的信息为number_of_exceptions
与exception_index_table
,而LineNumberTable
后续的信息为line_number_table_length
及line_number_table
,它们的格式和占用空间的大小都是不同的,只有依赖事先定义好的规范,才能将对应属性的正确信息解析出来。
tips: 这个表格比较复杂,建议有相关需求和兴趣的同学们直接查看官方的Java虚拟机规范文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7
4.总结
Class文件中记录了很多关键的信息,了解Class文件的结构能够帮助我们更深入地理解Java运行原理。
我们在最后对Class文件中的结构做一个简单的分类,大家一条一条看下去,也跟着思考一下,如果不保存这一信息会怎么样,会不会影响代码的运行?这个信息具体保存在Class文件的哪一部分?
Class文件结构分类:
- 结构信息
- Class文件格式版本号
- 各部分的数量及所占空间大小
- 元数据(对应Java源代码中“声明”和“常量”信息)
- 类 / 继承的超类 / 实现接口的声明信息
- 域 / 方法 的声明信息
- 常量池
- 运行期注解
- 方法信息(对应Java源代码中“语句”与“表达式”信息)
- 字节码
- 异常处理器表
- 操作数栈 与 局部变量区 的大小
- 符号信息(如LineNumberTable、LocalVariableTable)
我是敖丙,你知道的越多,你不知道的越多,下期见!
文章转载自公众号:敖丙