类文件结构

前言

我们知道我们写完的Java程序经过javac xxx.java编译后生成了xxx.class文件,那么现在我们就一起通过解析一个.class文件来深入的学习一下类文件结构。
备注:以下所有内容均整合于《深入理解jvm虚拟机》

Class类文件结构

简单介绍一下Class文件是什么

  • 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口不一定都需要定义在文件里(比如类或接口也可以通过类加载器直接生成)。
  • Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间没有添加任何分隔符。这使得整个Class文件中存储的内容几乎全是程序运行的必要数据。
  • Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,其中只有两种数据类型:无符号数和表。
  • 无符号数属于基本的数据类型,以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯以“_info”结尾。用于描述有层次关系的符合结构数据。
    整个Class文件本质就是一张表,它由下图所示的数据项构成(也就是一个Class文件由下列数据一项一项的拼接而成)
表2-1
## Java程序示例 首先看一段简单的Java程序示例,并将其编译成Class文件 ``` package com.example.demo;

public class TestClass {

private String str;

public String getStr(){
    return str + "Test";
}

}
下图是使用十六进制编辑器WinHex打开这个class文件的结果

魔数与Class文件的版本

首先重Class文件头开始看起,头4个字节称为魔数(Magic Number),可以清楚的看到开头四个字节为0xCAFEBABE,它的唯一作用:确定这个文件是否为一个被虚拟机接受的Class文件
代表此版本号的第五个和第六个字节值为0x0000,而主版本号为0x0039,也就是十进制的57(对应jdk的版本为jdk13)
注:Java的版本号是从45开始的,JDK1.1之后的每一个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容低版本的JDK。

常量池

紧接着主次版本号后面的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,也是class文件中第一个出现的表类型数据项目。
由于常量池中常量的数据量是不固定的,所以在常量池的入口放置一项u2类型的数据,代表常量池容量技术值(constant_pool_count)。此容量计数从1开始
也就是说,常量池中常量的个数是这个容器计数-1。设计者将0空出来的目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。class文件中只有常量池的容量计数是从1开始的,对于其它集合类型,比如接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。
常量池主要存放两大类常量:字面量(Literal)和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符
    Java代码在进行javac编译的时候并不像C和C++那样有连接这一步,而是在虚拟机加载class文件的时候进行动态连接。也就是说,在class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,虚拟机也就无法使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
    常量池中的每一项都是一个表,在JDK1.7之前有11中结构不同的表结构,在JDK1.7中为了更好的支持动态语言调用,又增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。
    这14个表的开始第一个字节是一个u1类型的tag,用来标识是哪一种常量类型。这14种常量类型所代表的含义如下:
表3-1

由class文件结构图可知:

常量池的开头两个字节0x0029是常量池的容量计数,这里是41,也就是说,这个常量池中有40个常量项。
首先看一下常量池中第一项常量,容量计数后面的第一个字节标识这个常量的类型,是0x0A,即10

查[3-1]表可知这个标识列属于CONSTANT_Methodref_info类型,即代表的是一个类方法的符号引用,CONSTANT_Methodref_info的结构如下,见表3-2

表3-2

按照这个结构,可知tag是标志位,用于区分常量类型。接下去name_index是2(0x0002),descriptor_index是3(0x0003)。这都是一个索引,指向常量池中的其他常量,其中name描述了这个方法的名称,descriptor描述了这个方法的访问标志(比如public、private等)、参数类型和返回类型。

上面分析了一个常量,为了避免繁琐,后续的19个常量计算借助于工具完成。在JDK的bin目录中,Java已经为我们提供了一个解析常量池的工具javap,我们可以通过javap -verbose class文件名,就可以自动帮我们解析了,下面是这个程序的解析结果:

C:\Users\user\Desktop>javap -verbose TestClass
警告: 文件 .\TestClass.class 不包含类 TestClass
Classfile /C:/Users/user/Desktop/TestClass.class
  Last modified 2020年1月25日; size 880 bytes
  SHA-256 checksum 69ca1079e58fc02cd1ac9294ceb6a2e2c67619fc9474e46e6771de8e27bff0a2
  Compiled from "TestClass.java"
public class com.example.demo.TestClass
  minor version: 0
  major version: 57
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // com/example/demo/TestClass
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 3
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/example/demo/TestClass.str:Ljava/lang/String;
   #8 = Class              #10            // com/example/demo/TestClass
   #9 = NameAndType        #11:#12        // str:Ljava/lang/String;
  #10 = Utf8               com/example/demo/TestClass
  #11 = Utf8               str
  #12 = Utf8               Ljava/lang/String;
  #13 = InvokeDynamic      #0:#14         // #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
  #14 = NameAndType        #15:#16        // makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
  #15 = Utf8               makeConcatWithConstants
  #16 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/example/demo/TestClass;
  #22 = Utf8               getStr
  #23 = Utf8               ()Ljava/lang/String;
  #24 = Utf8               SourceFile
  #25 = Utf8               TestClass.java
  #26 = Utf8               BootstrapMethods
  #27 = MethodHandle       6:#28          // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #28 = Methodref          #29.#30        // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #29 = Class              #31            // java/lang/invoke/StringConcatFactory
  #30 = NameAndType        #15:#32        // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #31 = Utf8               java/lang/invoke/StringConcatFactory
  #32 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #33 = String             #34            // \u0001Test
  #34 = Utf8               \u0001Test
  #35 = Utf8               InnerClasses
  #36 = Class              #37            // java/lang/invoke/MethodHandles$Lookup
  #37 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #38 = Class              #39            // java/lang/invoke/MethodHandles
  #39 = Utf8               java/lang/invoke/MethodHandles
  #40 = Utf8               Lookup

从上述代码清单可以看出,已经计算出40个常量,且第1项计算结果与我们手工计算一致。最终的结果是后面显示的java/lang/Object.""😦)V,而且我们会发现并没有在Java程序中出现,还有一些内容也没有在Java程序中出现,比如“[”、“V”、“LineNumberTable”等。这是自动生成的常量,但它们会被后面即将介绍到的字段表、方法表和属性表引用到,用来描述一些不方便使用固定字节表示的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?
最后,给出14种常量项的结构:

表3-3

访问标志

在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于标识一些类或接口层次的访问信息,包括:这个Class是接口还是类;是否定义为public;是否定义为abstract;如果是类的话,是否被声明为final等。具体的标志位以及含义如下表3-4:

表3-4


access_flags 中一共有16个标志位可以使用,当前只定义了8个,没有使用到的标志位要求一律为0。以TestClass为例
它的访问标志值是0x0021,查表可知,这是ACC_PUBLIC和ACC_SUPER值取或运算的结果。所以TestClass这个类的访问标志就是ACC_PUBLIC和ACC_SUPER,这一点我们可以在javap得到的结果中验证:

类索引、父类索引与接口索引集合

访问标志结束后,接下去的内容代表类索引、父类索引及接口索引集合。
首先类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,Class文件通过这三项数据来确定这个类的继承关系。

  • 类索引:用于确定这个类的全限定名
  • 父类索引:用于确定这个类的父类全限定名。由于Java语言不允许多重继承,所以父类索引只有一个。除了Object之外,所有Java类都有父类索引,因此除了Object外,所有Java类的父类索引都不为0。
  • 接口索引集合:用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中
    类索引、父类索引及接口索引集合都按顺序排列在访问标志之后,类索引和父类索引引用两个u2类型的索引值表示,它们各自指向一个CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
    看一下本例子中的代码:

    看Class文件中的内容,在标志位0x0021结束后的三个值,分别是0x0008,0x00002,0x0000,也就是类索引为8,父类索引为2,(由于没有实现接口)接口集合大小为0,
    以类索引为例子,首先通过值8找到指向CONSTANT_Class_info中的索引为10,常量池中索引是10的CONSTANT_Utf8_info的常量值即为全限定名(父类的全限定名也如此解析)如图所示:

    由于这个类没有实现接口,所以接口索引集合的容量计数是0。如果容量计数是0,就不需要存储接口的信息。

字段表集合

字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但不包括方法内部的局部变量。所以一般字段可以包括以下信息。

  • 字段的作用域(public,private,protected修饰符)
  • 是否是实例变量(static修饰符)
  • 可变性(final)
  • 并发可见性(volatile修饰符
  • 是否被序列化(transient修饰符
  • 字段的数据类型及字段名称
    这些信息中,各个修饰符可以用布尔值表示。而字段叫什么名字、字段被定义为什么类型数据都是无法固定的,只能用常量池中的常量来表示。下面先给出字段表的格式:

    其中的字段修饰符access_flags,和类中的access_flags类似,都是一个u2的数据类型。其中字段来说可以设置的标志位及含义如下:

    跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表字段的简单名称及字段的描述符。
  • 描述符的作用是描述字段的数据类型,方法的参数列表和返回值。根据描述符的规则,基本数据类型(byte,char,int及等等)以及代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象的全限定名表示,详见表3-5
**表3-5**

对于数组类型,每一个维度将使用一个前置的“[”字符来描述。比如定义一个java.lang.String[][]类型的二维数组,将记录为[[Ljava/lang/String,一个整形数组int[]将标记为[I。
当描述符用来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()内。比如方法void inc()的描述符是:()V。方法java.lang.String toString()的描述符是:()Ljava/lang/String。方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符是:([CII[CIII)I。
对于例子中TestClass.class文件来说,如图所示:

第一个u2类型的数据为容量计数器fields_count, 其值为0x0001,说明这个类只有一个字段数据,接下来跟着的为容量计数器access_flags,值为0x0002,代表private修饰符标志位为真。代表字段名称的name_index值为0x000B,从常量表中可以看到第11项常量是一个CONSTANT_Utf8_info类型的字符串,其值为"str"。代表描述符的descriptor_index的值为0x000C,指向常量池第12项,即Ljava/lang/String,由此可以推断原定义的字段为private string str;

方法表集合

Class文件中方法表的格式和字段表几乎采用完全一致的方式,方法表的结构也依次包括了,访问标志,名称索引,描述符索引,属性表索引几项。
方法表的访问标志和字段的不同,如下表(3-6)所示:

**表3-6**

到此处,可以看见方法表中的定义只有标志,名称索引,描述符索引表达清楚了,但方法里面的代码却不再此处。方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为"Code"的熟悉里面,也就是属性表的概念(通俗的讲就是方法体中的代码)
继续以原例子进行分析,如图所示

方法表集合的入口地址为:前两个字节是方法表集合中的院元素个数,这里是0x0002,所以有两个方法,按照字段的解析方法,可以得到每个方法的定义。分别是编译器添加的实例构造器和例子中的方法getStr()。先看第一个方法的访问标志值为0x0001,也就是ACC_PUBLIC为真,名称索引为0x0005,对应常量池得方法名为,描述符索引为0x0006,对应常量为()V,属性表索引为0x0001表示此方法的属性表集合有一项属性,属性名称索引为0x0011,对应常量为"Code",说明此属性是方法的字节码描述。

属性表集合

这是Class文件中最后的内容

posted @ 2020-01-25 14:38  遗失的岁月  阅读(273)  评论(0编辑  收藏  举报