同事:你能跟我聊聊class文件么?(下篇)

荣光因缘来
发布于 2023-8-8 11:14
浏览
0收藏

3.4 访问标记

等到常量池的部分结束后,紧随其后的就是访问标记了。访问标记其实很好理解,就是类上的修饰符,如final、abstract等。

这个部分用两个字节的空间来保存,其实这里十六进制值存储的也是约定好的枚举值,不同的枚举值对应不同的访问标记名:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

在上面这个例子1中,访问标记就是​​00 21​​:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

​00 21​​​对应的就是​​ACC_PUBLIC​​​以及​​ACC_SUPER​​​,意味着这个类是public修饰的类,而​​ACC_SUPER​​则代表该类有继承关系。这也很好理解,在Java中,所有的类都是Object类的子类嘛。

3.5 类索引(this_class)

顾名思义,类索引保存的是一个与类相关的索引,既然有索引那么肯定有数据,那么它指向的数据是哪呢,就是前面提到的常量池

在例1中,类索引紧跟访问标记​​00 21​​​之后,也就是对应的​​00 07​​,指向常量池中索引为7的位置:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

我们找到常量池中索引为7的位置,发现这个位置对应的其实是一个​​CONSTANT_Class_info​​,代表这是一个类的信息常量:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

类对应的类名是​​cp_info #36​​,我们继续找下去,可以看到,this_class最终指向的是一个字符串字面量,也就是本类的类名:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

3.6 超类索引(super_name)

与类索引的查询方法一致,在例子1中,对应的超类索引十六进制值为​​00 08​​:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

同样到常量池中取索引,得到最终的超类名为:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

所有的类都是Object类的子类,在这一步得到了验证。

3.7 接口索引(interface)

与前两个索引的查询方法一致,不过这里有一些特殊点,在例子1中,由于该类没有实现任何接口,所以接口表索引为​​00 00​​:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

还记得前面所说的常量池的索引从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等声明信息,同样是以枚举的方式记录:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

3.8.2 name_index

字段名索引。跟3.5中的类索引相似,​​name_index​​​记录的是常量池中的索引,最终可以通过​​name_index​​找到一个字符串常量名,也就是字段名。

比如,在例子1中,第一个字段的​​name_index​​如下:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

再到常量池索引为​​#9​​​的位置查找,最终得到的字段名就是​​a​​啦。

3.8.3 descriptor_index

字段类型索引。

跟字段名索引的功能类似,指向常量池中的一个字符串常量,但是为了节省空间,字段的类型是用简写方式表示的,例如:

  1. 基础类型,byte、int、char、float等这些简单类型使用一个大写字符来表示,B对应byte类型,I对应的是Integer,大部分类型对应都是自己本身的首字母大写,除了有两个特殊的——J对应的是long类型,Z表示boolean类型
  2. 引用类型使用L+全类名+;的方式来表示,为了防止多个连续的引用类型描述符出现混淆,引用类型描述符最后都加了一个分号";"作为结束,比如字符串类型String的描述符为​​Ljava/lang/String;​​。
  3. 数组类型用"["来表示,如字符串数组String[]的描述符为“​​[Ljava/lang/String;​​​”,同时该符号也可表示多维数组,如​​int[][]​​​就被表示为​​[[I​​。

在例子1中,变量a的​​descriptor_index​​​为​​00 0A​​​,也就是指向索引​​#10​​​的位置,最终找到变量a的类型为​​I​​,也就是Integer。

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

3.8.4 attributes_count与attributes(属性表)

​attributes_count​​​表示的是字段属性项个数,而​​attributes​​则是字段属性项集合。

​attributes​​​中由各类的​​attribute_info​​​组成,​​attribute_info​​​记录的是具体的属性信息,比较常见的有​​ConstantValue​​​属性,表示这个字段是一个常量;还有​​RuntimeVisibleAnnotations​​属性,表示该字段上标注有运行时注解,比如Spring相关注解。

关于运行时注解与编译时注解我们在编译过程中已经讨论过了,还有疑惑的同学可以回过去复习一下。

可以发现,字段表其实是一个嵌套式的表结构,​​field_info​​​表内部嵌套一个​​attributes​​。

我们回到Class文件中看看他们是怎么表示的,在例子1​​javap​​​结果中间的部分,在​​#37​​​之后的一行,有关于变量​​a​​的描述:

private final int a;
    descriptor: I
    flags: ACC_PRIVATE, ACC_FINAL
    ConstantValue: int 100

这里展示的就是变量​​a​​​对应的​​访问标记​​​、​​字段类型​​​以及​​属性表​​的内容啦。

在jclasslib中就看得更清楚了:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

这就是字段表中保存的关于变量a的相关信息了。

等等,这里有一个重点!!

我们在3.3 常量池的部分中抛出了一个问题,为什么int类型字段值只有声明为final后才会被保存到常量池中?

这里就能得到答案。

因为声明为final的字段,需要在​​attribute_info​​​中存储​​ConstantValue​​​属性来标识该字段是一个常量。而​​ConstantValue​​又需要记录下该字段的值。

这个值保存在哪个地方最适合呢,当然是常量池啦。

我们使用jclasslib查看就更直观了,常量值的索引指向​​cp_info #12​​​,而常量池中​​#12​​​位置存储的正是值​​100​​。

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

未被final声明的int类型,其值不会保存到常量池中,而是在使用时直接嵌入到字节码中,如本例子中的变量b:

同事:你能跟我聊聊class文件么?(下篇)-鸿蒙开发者社区

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文件的一部分,但它究竟在什么位置,现在就有了答案:

字节码记录在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文件最后的这个属性表记录的跟类的相关信息具体有哪些呢?

只说几个常见的:

  1. SourceFile:类的源文件名称。
  2. RuntimeVisibleAnnotations:类上标记的注解信息。
  3. 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文件结构分类:

  1. 结构信息
  • Class文件格式版本号
  • 各部分的数量及所占空间大小
  1. 元数据(对应Java源代码中“声明”和“常量”信息)
  • 类 / 继承的超类 / 实现接口的声明信息
  • 域 / 方法 的声明信息
  • 常量池
  • 运行期注解
  1. 方法信息(对应Java源代码中“语句”与“表达式”信息)
  • 字节码
  • 异常处理器表
  • 操作数栈 与 局部变量区 的大小
  • 符号信息(如LineNumberTable、LocalVariableTable)

我是敖丙,你知道的越多,你不知道的越多,下期见!




文章转载自公众号:敖丙

分类
标签
已于2023-8-8 11:14:48修改
收藏
回复
举报
回复
    相关推荐