JVM -类文件结构

下面的分析以如下的class文件为例:

CAFEBABE 00000034 00130A00 04000F09 00030010 07001107 00120100 016D0100 01490100 063C696E 69743E01 00032829 56010004 436F6465 01000F4C 696E654E 756D6265 72546162 6C650100 03696E63 01000328 29490100 0A536F75 72636546 696C6501 000E5465 7374436C 6173732E 6A617661 0C000700 080C0005 00060100 19636F6D 2F796F75 79652F63 6C617A7A 2F546573 74436C61 73730100 106A6176 612F6C61 6E672F4F 626A6563 74002100 03000400 00000100 02000500 06000000 02000100 07000800 01000900 00001D00 01000100 0000052A B70001B1 00000001 000A0000 00060001 00000003 0001000B 000C0001 00090000 001F0002 00010000 00072AB4 00020460 AC000000 01000A00 00000600 01000000 07000100 0D000000 02000E

Class文件结构

Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符若遇到需要占用8字节以上空间的数据时,则会按照高位在前的方式分割成若干组8位字节进行存储。

Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种结构只有两种数据类型:无符号数和表。

  • 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值
  • 表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾,整个Class文件本质上就是一张表。
ClassFile {
              u4             magic;                                          CAFEBABE
              u2             minor_version;                                  0000
              u2             major_version;                                  0034
              u2             constant_pool_count;                            0013
              cp_info        constant_pool[constant_pool_count-1];           0A00****6A656374
              u2             access_flags;                                   0021
              u2             this_class;                                     0003
              u2             super_class;                                    0004
              u2             interfaces_count;                               0000
              u2             interfaces[interfaces_count];                   无
              u2             fields_count;                                   0001
              field_info     fields[fields_count];                           0002****0000
              u2             methods_count;                                  0002
              method_info    methods[methods_count];                         0001****0009
              u2             attributes_count;
              attribute_info attributes[attributes_count];
}

魔数

魔数代表文件的格式,由于文件的后缀名可以随意更改,所以在文件的开头用CAFFBABY代表Class文件,这种定义广泛用于各种文件定义中。例如png的魔数为0x89504E470D0A1A0A,jpeg的魔数为0xFFD8FF

版本号

主版本从JDK1.0到JDK1.8以十进制表示为45-52。版本号指明生成该class文件的JDK版本,在上表中知道生成该class文件的主版本为0x0034及十进制的52,所以生成该class文件的JDK主版本为1.8。说明这个class文件是可以被1.8或以上版本虚拟机执行。

常量池

常量池中的常量数量是不固定的,所以在常量池的开始用一个u2长度的数据代表常量的个数。常量池的下标从1开始,0用于后面某些常量池的索引值得数据在特定情况下需要表达"不引用任何一个常量池项目"的含义。常量池中主要存放两大类常量:字面量和符号引用

  1. 字面量:比较接近于Java语言层面的常量概念。比如字符串、fianl常量值
  2. 符号引用: 属于编译原理方面的概念,其包含下面三类常量:
  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

实例中代表常量池常量个数的数据为0x0013,即十进制的19,所有常量池中常量的个数为 19 - 1 =18.

常量池的项目类型

常量池中每一项常量都是一个表,总共14种,这14种表都有一个共同的特点,就是表的开始第一位是一个u1类型标志位

常量池中14种数据类型的结构

实例中的常量项分析如下所示:

#1 = Methodref 
       tag    u1     标志位 10  0A
       index  u2     指向声明方法的类描述符CONSTANT_Class_info的索引项 第4个常量项 0004
       index  u2     指向名称及类型描述符CONSTANT_NameAndType的索引项 第15个常量项 000F

#2 = Fieldref
       tag    u1     标志位 9  09
       index  u2     指向声明字典的类或接口描述符CONSTANT_Class_info的索引项 第3个常量项     0003
       index  u2     指向字段描述符CONSTANT_NameAndType的索引项            第10个常量项    0010

#3 = Class
       tag    u1     标志位 7  07
       index  u2     指向全限定名常量项的索引 第17个常量项 0011

#4 = Class
       tag    u1     标志位 7  07
       index  u2     指向全限定名常量项的索引 第18个常量项 0012

#5 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 1个字节      0001
       bytes  u1     长度为length的UTF-8编码的字符串 长度为1的UTF-8编码的字符串 > m 6D

#6 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 1个字节      0001
       bytes  u1     长度为length的UTF-8编码的字符串 长度为1的UTF-8编码的字符串 > I 49

#7 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 6个字节      0006
       bytes  u1     长度为length的UTF-8编码的字符串 长度为6的UTF-8编码的字符串 > <init> 3C696E69743E

#8 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 3个字节      0003
       bytes  u1     长度为length的UTF-8编码的字符串 长度为3的UTF-8编码的字符串 > ()V 282956

#9 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 4个字节      0004
       bytes  u1     长度为length的UTF-8编码的字符串 长度为4的UTF-8编码的字符串 > Code 436F6465

#10 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 15个字节     000F
       bytes  u1     长度为length的UTF-8编码的字符串 长度为15的UTF-8编码的字符串 > LineNumberTable 4C696E65 4E756D62 65725461 626C65

#11 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 3个字节      0003
       bytes  u1     长度为length的UTF-8编码的字符串 长度为3的UTF-8编码的字符串 > inc 696E63

#12 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 3个字节      0003
       bytes  u1     长度为length的UTF-8编码的字符串 长度为3的UTF-8编码的字符串 > ()I 282949

#13 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 10个字节     000A
       bytes  u1     长度为length的UTF-8编码的字符串 长度为10的UTF-8编码的字符串 > SourceFile 536F7572 63654669 6C65

#14 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 14个字节     000E
       bytes  u1     长度为length的UTF-8编码的字符串 长度为14的UTF-8编码的字符串 > TestClass.java 54657374 436C6173 732E6A61 7661

#15 = NameAndType
       tag    u1     标志位 12 0C
       index  u2     指向该字段或方法名称常量项的索引           第7个常量项索引 0007 // <init>
       index  u2     指向该字段或方法描述符常量项的索引         第8个常量项索引 0008 // ()V

#16 = NameAndType
       tag    u1     标志位 12 0C
       index  u2     指向该字段或方法名称常量项的索引           第5个常量项索引 0005 // m
       index  u2     指向该字段或方法描述符常量项的索引         第6个常量项索引 0006 // I

#17 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 25个字节     0019
       bytes  u1     长度为length的UTF-8编码的字符串 长度为25的UTF-8编码的字符串 > com/youye/clazz/TestClass 636F6D2F 796F7579 652F636C 617A7A2F 54657374 436C6173 73

#18 = Utf8
       tag    u1     标志位 1 01
       length u2     UTF-8编码的字符串占用的字节数 16个字节     0010
       bytes  u1     长度为length的UTF-8编码的字符串 长度为16的UTF-8编码的字符串 > java/lang/Object 6A617661 2F6C616E 672F4F62 6A656374

访问标识符

访问标志用来标识类或接口的访问信息,包括Class是类还是接口,是否定义为public;是否定义为abstract;如果是类的话是否被申明为final等。具体的标志位以及含义如下所示

标志名称                 标志值                  含义
ACC_PUBLIC              0x0001                是否为public类型
ACC_FINAL               0x0001                是否被声明为final,只有类可设置
ACC_SUPER               0x0020                是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真。
ACC_INTERFACE           0x0020                标识这是一个接口
ACC_ABSTRACT            0x0400                是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC           0x1000                标志这个类并非由用户代码产生的
ACC_ANNOTATION          0x2000                标识这是一个注解
ACC_ENUM                0x0400                标识这是一个枚举

在前面已经知道这个类是在JDK1.8.0下编辑的,所以ACC_SUPER 必须为真。则有 0x0021 ^ 0x0020 = 0x001, 即该类是由public修饰的。
再比如 该类的访问标识符为 0x0031,则有 0x0031 ^ 0020 = 0x0011。查表可以知道 0x0001 | 0x0010 = 0x0011。及标识该类是由 public final修饰的。

类索引

确定这个类的全限定名,它指向一个类型为CONSTANT_Class_info的类描述符常量,根据CONSTANT_Class_info类型中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

父类索引

确定这个类的父类全限定名。它同样指向一个类型为CONSTANT_Class_info的类描述符常量。在Java中除了java.lang.Object无父类外,其他的类都有父类,所以父类索引都不为0。

接口计数器和接口索引集合

接口计数器标识该Class中含有多少接口,紧跟着接口计数器的是接口索引集合。接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
而接口计数器表示接口索引表的容量。如果这个类没有实现接口,则计数器值为0,后面接口的接口索引表不再占用任何字节。

字段数量

字段包括类级别变量和对象级别变量,不包括方法内部定义的局部变量。

字段结构表

紧接着字段数量的是一个字段表结构,字段的作用域(private、protected、public),是实例变量还是对象变量(static修饰符),可变性(final修饰符),并发可见性(volatile修饰符)等等都在字段表接口中体现。
其中字段修饰符access_flags,和类中的access_flags类似,对于字段来说可以设置的标志位及含义如下:

标志名称                 标志值                  含义
ACC_PUBLIC              Ox0001                 字段是否是public
ACC_PRIVATE             0x0020                 字段是否是private
ACC_PROTECTED           0x0004                 字段是否是protected
ACC_STATIC              0x0008                 字段是否是static
ACC_FINAL               0x0010                 字段是否是final
ACC_VOLATILE            0x0040                 字段是否是volatile
ACC_TRANSIENT           0x0008                 字段是否是transient
ACC_SYNTHETIC           0x1000                 字段是否是由编译器自动产生的
ACC_ENUM                0x4000                 字段是否是enum

参数类型使用一个大写字母来表示如下表所示:

标识字符                含义             标识字符                 含义
B                      byte               J                    long
C                      char               S                    short
D                      double             Z                    boolean
F                      float              V                    void
I                      int                L                    对象类型,如Ljava/lang/Object

对于数组类型,每个一维数组将使用一个前置的’[‘字符来描述。比如定义一个java.lang.String[][]类型的二维数组,将记录为’[[Ljava/lang/String’,一个double数组 double[]记为’[D’。

filed_info {
       access_flags         u2                   1                          0x0002 > private
       name_index           u2                   1                          0x0005 > m
       descriptor_index     u2                   1                          0x0006 > I
       attributes_count     u2                   1                          0x0000 > 属性表集合中无属性
       attributes           attribute_info       attributes_count
}

根据上表,则可以推出定义这个字段的源代码为 private int m;

方法数量

与字段表集合相同的是,如果父类中的方法没有在子类中被重写,是不会出现在方法表中。但有可能会出现编译器自动添加的方法。

方法表结构

class文件存储格式中对方法的描述和对字段的描述几乎相同,方法表的结构也和字段表相同。不过方法表的访问标志和字段的不同,列出如下:

标识名称                 标志值                               含义
ACC_PUBLIC              0x0001                         方法是否是public
ACC_PRIVATE             0x0002                         方法是否是private
ACC_PUBLICPROTECTED     0x0004                         方法是否是protected
ACC_STATIC              0x0008                         方法是否是static
ACC_FINAL               0x0010                         方法是否是final
ACC_SYNCHRONIZED        0x0020                         方法是否是synchronized
ACC_BRIDGE              0x0040                         方法是否是由编译器产生的桥接方法
ACC_VARARGS             0x0080                         方法是否接受不定参数
ACC_NATIVE              0x0100                         方法是否是native
ACC_ABSTRACT            0x0400                         方法是否是abstract
ACC_STRICTFP            0x0800                         方法是否是strictfp
ACC_SYNTHETIC           0x1000                         方法是否是由编译器自动产生的
method_info {
       access_flags         u2                   1                          0x0001 > public
       name_index           u2                   1                          0x0007 > <init>
       descriptor_index     u2                   1                          0x0008 > ()V
       attributes_count     u2                   1                          0x0001 > 属性表集合有一项属性
       attributes           attribute_info       attributes_count           0x0009 > 属性表集合中的一个属性名称索引为第9个常量项,即Code,说明此属性是方法的字节码描述 
}

表中的Code属性在后面在分析

method_info {
       access_flags         u2                   1                          0x0001 > public
       name_index           u2                   1                          0x000B > inc
       descriptor_index     u2                   1                          0x000C > ()I
       attributes_count     u2                   1                          0x0001 > 属性表集合有一项属性
       attributes           attribute_info       attributes_count           0x0009 > 属性表集合中的一个属性名称索引为第9个常量项,即Code,说明此属性是方法的字节码描述 
}

Java类都要有一个构造方法,如果没有的话编译器会自动构造一个无参的构造方法,就是上面的第一个名叫的方法;同时,如果一个类中含有静态代码块或者静态变量,那么就需要首先执行类的构造方法,来执行静态代码块和初始化静态变量,这就是上面的第三个名为的方法。

不过,方法比字段还多了方法体呢,那方法体中的代码哪去了?

在每一个方法表中descriptor_index后描述属性的时候,0001表明属性的个数为1,再后面的000E是指向常量池中的CONSTANT_Utf8_info常量,内容是Code,说明后面属性中存放的就是方法体里面编译后的字节码指令。

在Java中,要重载一个方法,除了要与原方法具有相同的方法名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是特征签名只包含参数个数和类型,并不包含返回值类型,所以Java语言中是无法仅仅依靠返回值的不同来对一个方法重载的。但是在class文件格式中,特征签名还包括返回值类型,也就是说只有返回值类型不同的两个方法也可以存在。这一点在泛型中编译后类型擦除后生成的桥接方法上有所体现。不过这里就不过多介绍了。

属性数量和属性集合表

最常用的属性恐怕就是Code属性了,因为大多数的方法都会有编译后的字节码指令,这些指令就存储在方法表中的Code属性中。如果一个Java程序的信息可以分为代码(方法体中的代码)和元数据(包括类、字段、方法定义以及其它信息),那么Code属性存储的就是代码,其它所有的结构存储的都是元数据。不过并非所有的方法表都有这个Code属性,比如接口或抽象类中的方法表就不存在Code属性(JDK 1.8中的接口也可以定义方法了)

上面两个方法中的两个属性分别为

Code {
       attribute_name_index        u2 0009
       attribute_length            u4 0000001F
       max_stack                   u2 0001
       max_locals                  u2 0001
       code_length                 u4 00000005
       code                        x*u1 2AB70001 B1
       exception_table_length      u2 0000
       exception_table             exception_info 无
       attributes_count            u2 0001
       attribute_info              attributes 000A00 00000600 01000000 03
}

LineNumberTable {
       attribute_name_index        u2 000A
       attribute_length            u4 00000006
       line_number_table_lenght    u2 0001
       line_number_table           line_number_info line_number_table_length 0000 0003
}

Code {
       attribute_name_index        u2 0009
       attribute_length            u4 0000001F
       max_stack                   u2 0002
       max_locals                  u2 0001
       code_length                 u4 00000007
       code                        x*u1 2AB40002 0460AC
       exception_table_length      u2 0000
       exception_table             exception_info 无
       attributes_count            u2 0001
       attribute_info              attributes 000A00 00000600 01000000 03
}

LineNumberTable {
       attribute_name_index        u2 000A
       attribute_length            u4 00000006
       line_number_table_lenght    u2 0001
       line_number_table           line_number_info line_number_table_length 0000 0007
}

line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_cp 和 line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号

SourceFile属性记录生成这个class文件的源码文件名称。在上面的数据中,0001表示属性表集合中有一个属性,000D(即十进制13)是属性名的索引值,查找常量池可以知道是SourceFile,00000002是这个属性的长度,即两个字节,最后的两个字节就是这个属性的内容,是一个常量池索引,000E,十进制14,结果是Test.java。

至此Test.class以分析完毕

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值