「Java 路线」| Class 文件结构

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 「Java 路线」| 导读 —— 他山之石,可以攻玉 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • Class 文件Java技术体系的重要组成部分,在学习整个虚拟机的执行引擎之前,应该清楚Class 文件的结构;
  • 这篇文章将带你理解Class 文件的基本结构,希望能帮上忙。

延伸文章

目录


1. 什么是 Class 文件?

  • 定义:或称字节码,可以看作Java 虚拟机的可执行文件

  • 作用:对应于一个类 / 抽象类 / 接口的定义信息

  • 意义Java 虚拟机 & Class 文件共同构成了Java无关性的基础

  • 来源

      1. Java 源码经过Java 编译器编译后得到,以磁盘文件形式存在
      1. 由字节码生成技术(如javassist / CGLib / ASM)生成,以内存中二进制流形式存在
# 咬文嚼字 #

虽然字节码不一定是以 “磁盘文件” 的形式存在,但是通常在很多文献 & 资料中会笼统地将字节码表述为Class 文件,这里不必钻牛角尖。

更多信息请务必阅读:《Java | 为什么 Java 实现了平台无关性》


2. Class 文件的大致结构

  • Class 文件是一种强协议的紧凑型结构(遵循《Java 虚拟机规范》)
  • Class 文件有三种数据结构:无符号数、TVL、表,具体如下:
  • Class 文件本质上也是一个表,大致结构如下图:
  • 魔数:固定值0xCAFEBABE,用于鉴别为合法的Class 文件
  • 版本号:表示Class 文件的目标版本号,高版本的虚拟机可以向前兼容旧版本Class 文件
  • 访问标志:一个u2无符号数,用于表示本类 / 接口的访问信息。其中每个标志位的值与java.lang.reflect.Modifier中的常量一一对应:
  • 本类索引 & 父类索引 & 接口索引集:是一个索引值,(共经过 2 次索引后)指向常量池中一个utf-8编码的 全限定名,例如:java/lang/Object;

  • 字段表集合:用于描述类或接口中声明的变量(包括类变量与成员变量)

  • 方法表集合:用于描述类或接口中声明的方法(包括类方法与成员方法)

  • 属性表集合:用于描述 Class 文件、字段表、方法表中携带的属性


下面,我将概括表中各个重点数据项的具体含义!

3. 常量池(const pool)

  • 常量池中的每一项常量都是一个表
  • 存放两种类常量:字面量(Literal)符号引用(Symbolic References)

符号引用(Symbolic References)是一个字符串类型的字面量(存储在常量池),它的作用是唯一地标示一个实体,最重要的特点如下:

  • 平台无关性
    这一点与Java的特性是一脉相承的。符号引用与具体虚拟机实现内存布局无关,需要在运行期将符号引用转换为直接引用(Direct Reference),这个直接引用才是符号引用在虚拟机中的真实存在。

  • 唯一性
    无歧义地标示一个目标,以方法为例,如果是本类中声明的方法,不需要添加类名(如:Method func:()V);如果是其他类中声明的方法, 需要添加类名前缀(如:Method com/Base.func:()V)。

常量池表结构有一个共同的特点,就是表结构的首元素是u1的标志位,代表当前的常量类型,截止到Java 13总共有 20 种常量:

常量类型 —— 引用自《深入理解Java虚拟机》

4. 本类索引 & 父类索引 & 接口索引集

  • 本类索引肯定存在,只有一个
  • Java是类单继承,所以父类索引只有一个(特例:Object的父类索引为 0)
  • Java是接口多继承,所以接口索引有零或多个

这三个索引值均指向常量池中CONSTANT_Class_info常量,而CONSTANT_Class_info常量本质上也是一个索引值,指向CONSTANT_Utf8_info常量。经过 2次 索引,这三个索引最终指向对应 类 / 接口的全限定名

2 次索引后指向全限定名 —— 引用自《深入理解Java虚拟机》

5. 字段表(field_info)

字段表用于描述类字段与实例字段,但只包括在本类中声明的字段,既不包括父类中声明的字段,也不包括方法内部声明的局部变量。要注意的是,编译器生成的字段是包括的,例如编译器会为非静态内部类添加外部类的引用字段。字段表的基本结构如下:

字段表的基本结构
  • access_flags:字段的访问标志位
  • name_index:常量池索引,最终指向字段的简单名称,见第 8 节
  • descriptor_index:常量池索引,最终指向字段的描述符,见第 8 节
  • attributes_count & attributes:字段属性,为字段的附加信息,见第 8 节

6. 方法表(method_info)

方法表和字段表的设计是很相似的。方法表用于描述类方法与实例方法,但只包括在本类中声明的方法或者重写的方法,不包括父类或父接口中声明的方法。需要注意的是,编译器生成的方法是包括的,例如类构造器<clinit>()与实例构造器<init>()。方法表的基本结构如下:

方法表的基本结构

可以看到,方法表和字段表的基本结构是完全一致的,此处不再赘述。需要特别指出的是,方法里面的代码在方法的Code属性,方法的受检异常声明在Exception属性


7. 属性表(attribute_info)

  • 属性相当于字段表或方法表的附加信息
  • 每一项属性都是一个表,基本结构如下图所示:
属性表的基本结构
  • attribute_name_index:常量池索引,最终指向一个属性名。Class 文件使用属性名来区分每一种属性,截止到Java 12,总共有 29 种预定义属性。
  • attribute_length:不同属性的属性信息是不同的,因此需要一个长度表表示属性信息占用的长度
  • info:属性信息
# 你觉得呢?#

市面上你能找到的介绍虚拟机的书籍,普遍都会按顺序罗列出每种属性的信息。笔者并不是说这种方式不好,因为作为书籍的阐述方式需要考虑到读者可接受度 & 参考性的问题。但是如果以博客的阐述方式也采用同样地方式,岂非成为知识搬运工?因此,我将从不同的问题域出发,在每个问题域中介绍每种属性。

Code 属性
Exceptions 属性

LocalVariableTable 属性

LocalVariableTypeTable 属性
Signature 属性

泛型中所谓的类型擦除,其实只是擦除Code 属性中的泛型信息,在类常量池属性中其实还保留着泛型信息,这也是在运行时可以反射获取泛型信息的根本依据。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!


RuntimeVisibleAnnotations 属性
RuntimeInvisibleAnnotations 属性
RuntimeVisibleParameterAnnotations 属性
RuntimeInvisibleParameterAnnotations 属性

注解在编译后擦除,如果注解的保留级别为CLASS & RUNTIME,在 Class 文件中还会生成对应的注解属性。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!


InnerClasses 属性

8. 信息描述规则

Editting...


参考资料

  • 《深入理解Java虚拟机(第3版本)》(第6章)—— 周志明 著
  • 《深入理解Android:Java虚拟机 ART》(第2章) —— 邓凡平 著
  • 《深入理解 JVM 字节码》(第2、3、4章)—— 张亚 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

推荐阅读更多精彩内容