[JVM]类文件结构

183 阅读8分钟

这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战

字节:1字节 = 8位bit。

类文件结构

大概的构造可见:未命名文件 - ProcessOn

魔数、主次版本号

魔数4字节:0xCAFEBABE。

主版本号2字节(假设为X):X-44 = JDK(Y)

副版本号2字节:标识特性(JDK12后启用)

常量池

常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它 还是在Class文件中第一个出现的表类型数据项目。

由于常量池包括的内容数量不固定,因此在常量池前,给了一个U2(2字节)来记录常量池容量计数值,从1开始计数,最多可以表示65534个。

在Class文件格式规范制定之时,设计者将第0项常量 空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下 需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译 原理方面的概念,主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)

  • 类和接口的全限定名(Fully Qualified Name)

  • 字段的名称和描述符(Descriptor)

  • 方法的名称和描述符

  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)

  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

访问标志

常量池之后,接着2个字节代表访问标志,用来记录一些类、接口层次的信息,包括:

  • Class是类还是接口
  • 是否是public
  • 是否是abstract
  • 是否是final

类名以及继承关系

这里包括:

  • 类索引(确定类的全限定名 - 在常量池中)
  • 父类索引(确定父类的全限定名 - 在常量池中)
  • 接口索引集合
    • 由于java支持多实现,因此这里需要2个字节记录实现接口数量
    • 随后的2*n个字节,描述接口的全限定名 - 在常量池中

字段表集合

这里描述的范围是

包括类级变量、实例级变量,不包括方法内部的局部变量

字段表结构为:

  • 字段数
  • 字段表集合
    • 访问标志
    • 名称索引
    • 描述符索引
    • 变量个数
    • 变量信息

字段访问标志

这里和类的访问标志类似,描述的是字段的属性,包括:

  • 访问权限(public、private、protected)
  • 可见域(static)
  • 是否可变(final)
  • 线程可见性(volatile)
  • 可序列化(transient)
  • 是否有编译器自动产生(synthetic)
  • 是否为枚举(enum)

名称索引、描述符索引

这里和类继承关系中的类似,也是常量池中引用。

  • 名称索引代表的是字段的简单名称

  • 描述符索引代表的是字段和方法的描述符

简单名称、描述符、全限定名见附录。

变量信息

这部分内容放在属性表集合中。

方法表集合

方法表的结构和上面的字段表几乎是相同的,都是由一个描述个数的字段开始,后面带着若干的方法表。

  • 需要注意的是,方法表不可能为空,因为即使没有方法,编译器都会提供一个类构造器和实例构造器。

  • 方法表中方法里具体的代码,放到了方法属性表的名为Code的属性中。

属性表集合

属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以 携带自己的属性表集合,以描述某些场景专有的信息。

属性表中,不再严格要求顺序,只要不和已有的重名,任何编译器都可以往属性表中写入自定义信息。JVM运行时,只会解析它认识的信息,丢弃那些不认识的。

属性表中,包括了大量上述几个区域中长度无法预先定义的内容,其中就包括整个类文件最重要的code属性表

Code属性表

Code属性存放的是经过Javac编译器处理之后变为字节码Java程序方法体中的代码

Code属性表出现在方法表的属性集合之中。

并非所有的方法表都必须存在Code属性表,例如接口或抽象类中的方法,就不存在Code属性。

为什么说Code属性表是Class文件中最重要的属性?

  • 因为一个Java文件包括的信息,可以分为代码元数据(描述类、字段、方法定义和一些具体的定义信息),代码属性有且仅由Code属性表描述,其他的是用来描述元数据的。

其中,每个字段都有些特定的规则:

maxLocals
  • max_locals: U2,单位变量槽,对于byte、char、float、int、short、boolean和 returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。
    • 这个大小是根据同时生存的最大局部变量决定的,而不是所有的局部变量。
code_length和code

存储源程序编译后的字节码指令。

code只有1个字节,代表的是编译出的一条字节码指令。

code_length虽然有4字节,但java虚拟机规范只允许65535,超过了javac就拒绝编译了。

异常表

如果代码中没有显式处理异常,那么异常表中没有信息。

异常表描述的是:在代码的哪个位置,如果抛出了哪个异常,则跳转到哪个指令处理。

同样地,异常表中具体的信息,也是通过一个指明数量的数加上一堆指向常量池ClassInfo的指针确定。

常数值属性(ConstantValue)

ConstantValue属性在属性表中的使用范围是针对字段表的。

字段表中,如果为静态字面量值赋值了,那么字段表中就会有指向这个区域的对应信息

这里的记录信息也较为简单,最终的指向依然是依靠对常量池的值的引用。

因此,这里的类型就只能局限于基本类型以及String,这是由常量池的特性决定的。

字段属性

这个属性的作用是通知虚拟机自动为静态变量赋值

也就是说:只有被static修饰的基本变量或String才可以使用这个区域。

对于非静态变量(实例变量),赋值是在实例构造器方法中进行的。

对于类变量(也就是静态变量),这里的初始化有2种情况(这是Oracle的javaC编译器的实现,java虚拟机规范并不强制要求是final):

  • 如果同时使用final和static修饰,且是基本类型或String,那么就会生成ConstantValue属性进行初始化;
  • 如果不满足上面的条件,那么就会在中初始化。

由于书中内容很多,这里只挑了一些日常中比较能够接触到的进行记录,如果后面需要,再进行补充。

附录

全限定名、描述符、简单名称

全限定名

以类org.fenixsoft.clazz.TestClass为例,全限定名为org/fenixsoft/clazz/TestClass

为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。

简单名称

简单名称则就是指没有类型参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。

描述符

描述符用来描述字段的数据类型、方法的参数列表(数量、类型、书匈奴)、防范的返回值。

  • 基本数据类型以及void都用大写字符表示,对象用L+全限定名表示。
字符含义字符含义
BbyteJlong
CcharSshort
DdoubleZboolean
FfloatVvoid
IintL对象,例如Ljava/lang/Object;
  • 数组:在数据类型前用**一个[**描述,例如:
    • String[] [] - > [[Ljava/lang/String;
    • int[] -> [I

而描述一个方法时,顺序是:

  • 参数列表,用()包起来
  • 返回值

例如:

  • void ()方法的描述符:()V
  • String toString():()Ljava/lang/String;