同事:你能跟我聊聊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)
 
我是敖丙,你知道的越多,你不知道的越多,下期见!
文章转载自公众号:敖丙




















