美文网首页Java基础
class(一) 字节码文件结构

class(一) 字节码文件结构

作者: Timmy_zzh | 来源:发表于2020-11-21 22:32 被阅读0次
    1. class文件与虚拟机
    2. .class文件内容
    3. 常量池解析(重点掌握)

    1.Class字节码文件由来

    • Java能够实现“一次编译,到处运行”,其中class文件要占大部分功劳。

      • 为了让Java语言具有良好的跨平台能力,Java提供了一种可以在所有平台上都能使用的一种中间代码-字节码类文件(.class文件)
      • 有了字节码,无论哪种平台(Mac,Windows,Linux等),只要安装了虚拟机都可以直接运行字节码
    • 有了字节码,也解除了Java虚拟机和Java语言之间的耦合

      • Java虚拟机也支持很多除Java语言以外的其他语言,如Groovy,JRuby,Jython,Scala等,因为这些语言经过编译之后也可以生成能够被JVM解析并执行的字节码文件。
    • 虚拟机不关系字节码是由那种语言编译而来的。

    1.字节码与虚拟机解耦.png

    2.class文件组成

    class文件里只有两种数据结构:无符号数和表

    • 无符号数:属于基本的数据类型
      • 以u1,u2,u4,u8。来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,
      • 无符号数可以用来描述数字,索引引用,数量值或者字符串
    • 表:
      • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型。
      • class文件中所有的表都以"_info"结尾,整个class文件本质上就是一张表。

    表和无符号数之间的关系:

    2.表和无符号数之间的关系.png

    伪代码如下:

    // 无符号数
    u1 = byte[1];
    u2 = byte[2];
    u4 = byte[4];
    u8 = byte[8];
    
    // 表
    class_table {
        // 表中可以引用各种无符号数,
        u1 tag;
        u2 index2;
        ...
        // 表中也可以引用其它表
        method_table mt;
        ...
    }
    

    3.class文件结构

    • class文件中只存在无符号数和表两种数据结构。

      • 这些无符号数和表组成了class中的各个结构;
      • 这些结构按照预先规定好的顺序紧密的从前向后排列,相邻的项之间没有任何间隙。
    • class文件结构

    魔数 版本号 常量池 访问标志 类/父类/接口 字段描述集合 方法描述集合 属性描述集合

    当JVM加载某个class文件时,jvm就是根据上图中的结构去解析class文件,加载class文件到内存中,并在内存中分配相应的空间。具体某一种结构需要占用多大空间,如下图:

    字段 名称 数据类型 数量
    magic number 魔数 u4 1
    major version 主版本号 u2 1
    minor version 副版本号 u2 1
    constant_pool_count 常量池大小 u2 1
    constant_pool 常量池 cp_info constant_pool_count-1
    access_flag 访问标志 u2 1
    this_class 当前类索引 u2 1
    super_class 父类索引 u2 1
    interfaces_count 接口索引集合大小 u2 1
    interfaces 接口索引集合 u2 interfaces_count
    fields_count 字段索引集合大小 u2 1
    fields 字段索引集合 field_info fields_count
    methods_count 方法索引集合大小 u2 1
    methods 方法索引集合 method_info methods_count
    attributes_count 属性索引集合大小 u2 1
    attributes 属性索引集合 attribute_info attributes_count
    • 无符号数,表,class结构关系
      • class文件中的无符号和表相当于人类身体中的H,O,C,N等元素
      • 而class结构图中的各项结果相当于人类身体的各个器官
      • 并且这些器官的组织顺序是有严格顺序要求的

    实例分析

    import java.io.Serializable;
    
    public class Test implements Serializable, Cloneable {
    
        private int num = 1;
        public int add(int i) {
            int j = 10;
            num = num + i;
            return num;
        }
    }
    
    • 通过javac编译,生成Test.class字节码,然后使用16进制编辑器打开class文件,查看内容
      • 下图中都是一些16进制数字,每两个字符代表一个字节
    3.class内存(16进制).png
    • 其Test.class文件对应的字节码文件:javac -> javap -v Test.class
    7.class字节码文件1.png 7.class字节码文件2.png
    魔数 magic number
    CA FE BA BE
    
    • class文件开头的四个字节时class文件的魔数,它是一个固定的值--0XCAFEBABE
    • 魔数是class文件的标志,是判断一个文件是不是class格式文件的标准,如果开头四个字节不是0XCAFEBABE,就说明不是class文件,不能被JVM识别或加载
    版本号
    00 00 00 34
    
    • 跟在魔数后面的四个字节代表当前class文件的版本号。
      • 前两个字节0000代表次版本号(minor_version),后两个字节0034是主版本号(major_version),对应十进制值为52.
      • 也就是说当前class文件的主版本号为52,次版本号为0。也就是jdk1.8.0
    常量池
    • 紧跟在版本号之后的是一个叫做常量池的表(cp_info).
    • 在常量池中保存了类的各种相关信息,比如类的名称,父类的名称,类中的方法名,参数名称,参数类型等,这些信息都是以各种表的形式保存在常量池中的

    常量池中的每一项都是一个表,其项目类型共有14种,如下表:

    表名 标识位(Tag) 描述
    CONSTANT_utf8_info 1 UTF_8编码字符串表
    CONSTANT_Integer_info 3 整形常量表
    CONSTANT_Float_info 4 浮点型常量表
    CONSTANT_Long_info 5 长整型常量表
    CONSTANT_Double_info 6 双精度浮点型常量表
    CONSTANT_Class_info 7 类/接口 引用表
    CONSTANT_String_info 8 字符串常量表
    CONSTANT_Fieldref_info 9 字段引用表
    CONSTANT_Methodref_info 10 类的方法引用表
    CONSTANT_InterfaceMethodref_info 11 接口的方法引用表
    CONSTANT_NameAndType_info 12 字段或方法的名称和类型表
    CONSTANT_MethodHandle_info 15 方法句柄表
    CONSTANT_MethodType_info 16 方法类型表
    CONSTANT_InvokeDynamic_info 18 动态方法调用表
    • 常量池种的每一项都会由一个u1大小的tag值,tag值是表的标识,jvm解析class文件时,通过这个值来判断当前数据数据结果是哪一种表。

    以CONSTANT_Class_info和CONSTANT_utf8_info两张表距离说明:

    CONSTANT_Class_info表具体结果如下:

    table CONSTANT_Class_info {
        u1  tag = 7;
        u2  name_index;
    }
    
    • 解析
      • tag:占用一个字节大小。不如值位7,说明是CONSTANT_Class_info类型表
      • name_index:是一个索引值,可以将它理解为一个指针,指向常量池种索引为name_index的常量表。比如name_index=2,则它指向常量池种第2个常量

    CONSTANT_utf8_info表具体结果如下:

    table CONSTANT_utf8_info {
        u1  tag;
        u2  length;
        u1[] bytes;
    }
    
    • 解析
      • tag:值为1,表示是CONSTANT_utf8_info类型表
      • length:length表示u1[]的长度,比如length=5,则表示接下来的数据是5个连续的u1类型数据
      • bytes:u1类型数组,长度为length的值
    • 在java代码中声明的String字符串最终在class文件中的存储格式就是CONSTANT_utf8_info。因此一个字符串最大长度就是u2所能代表的最大值65535(2的16次方),但是需要使用2个字节来保存null值,因此一个字符串的最大长度为65535-2=65534.

    在常量池内部的表中也有相互之间的引用。用一张图来理解CONSTANT_Class_info和CONSTANT_utf8_info表格之间的关系,如图:

    4.常量池中表之间的关系.png
    接着往下解析,常量池元素个数
    • 因为开发者平时定义的java类各式各样,类中的方法与参数也不尽相同。所以常量池的元素数量也就无法固定。
    • 因此class文件在常量池的前面使用2个字节的容器计数器,用来代表当前类中常量池的大小。
    • 如下图,红色框中的0017转换为十进制为23,也就是说常量计数器的值为23.其中下标为0的常量被jvm留作其他用途,因此Test.class中实际常量池大小为这个计数器的值减1,为22个
    5.常量池数量大小.png
    第一个常量,如下图
    6.常量池第一个常量.png
    • 0A转化为十进制为10,通过查看常量池14种类型表格,tag=10的表类型为CONSTANT_Methodref_info,因此常量池种的第一个常量类型为方法引用表,结果如下:
    CONSTANT_Methodref_info {
        u1 tag = 10;
        u2 class_index;        指向此方法的所属类
        u2 name_type_index;    指向此方法的名称和类型
    }
    
    • 也就是说在“0A”之后的2个字节指向这个方法是属于哪个类的,紧接的2个字节指向这个方法的名称和类型,他们的值分别为:
      • 0004:十进制4,表示指向常量池种的第4个常量
      • 0011:十进制17,表示指向常量池中的第17个常量
    • 根据上面javap查看的字节码内容,可以知道:
      • 变量1表示方法引用表,为Object类的init构造方法
    6.常量池第一个常量表示.png
    接着看第二个常量
    8.常量池第二个常量.png
    • tag为09,为字段引用表为CONSTANT_Fieldref_info,其结果如下
    CONSTANT_Fieldref_info{
        u1 tag;
        u2 class_index;        指向此字段的所属类
        u2 name_type_index;    指向此字段的名称和类型
    }
    
    • 后面是4个字节都是索引

      • 00 03:指向常量池中第3个常量
      • 00 12:指向常量池中第18个常量
    • 字节码表示

      • 表示第二个常量为字段,且该字段属于Test类,字段名称为num,类型为I
    8.常量池第二个常量字节码表示.png

    其他常量也使用同样方式解析

    4.访问标志

    • 紧跟在常量池之后的常量是访问标志,占用两个字节


      10.类索引与父类索引.png
    9.访问标志.png
    • 访问标志代表类或者接口的访问信息:
      • 比如该class文件是类还是接口,是否被定义为public,是否是abstract,如果是类,是否被声明为final等等。
      • 各种访问标志如下所示:
    访问标志 描述
    ACC_PUBLIC 0X0001 public类型
    ACC_FINAL 0X0010 被声明为final类型的类
    ACC_SUPER 0X0020 是否允许使用invokespecial字节码指令的新语义,默认为真
    ACC_INTERFACE 0X0200 标志这时一个接口类型
    ACC_ABSTRACT 0X0400 标志这是一个抽象类或是接口类型
    ACC_ANNOTATION 0X2000 标志这时一个注解
    ACC_ENUM 0X4000 标志这时一个枚举
    • 我们定义的Test.java是一个普通的Java类,不是接口,枚举或注解,并且被public修饰,但没有被声明为final和abstract,因此它对应的access_flags为0021(0X0001和0X0020相结合)

    5.类索引,父类索引与接口索引计数器

    在访问标志后的2个字节是类索引,类索引后的2个字节就是父类索引,父类索引后的2个字节则是接口索引计数器,如下图:

    10.类索引与父类索引.png
    • 可以看出类索引指向常量池中的第3个,父类索引指向常量池中的第4个索引,并且实现的接口个数为2个:

    看字节码对应内容:

    10.2类-父类-接口索引.png
    • 从图中可以看出,第3个常量和第4个常量均为CONSTANT_Class_info表类型,并且代表的类分别为“Test”和“Object”。
    • 再看接口计数器的值为2,代表这个类实现了2个接口,查看在接口计数器之后的4个字节分别为:
      • 0005:指向常量池中的第5个常量,从图中可以看出第5个常量值为Serializable
      • 0006: 指向常量池中的第6个常量,从图中看出第6个常量值为Cloneable
    • 综上所述,可以得出如下结论:当前类为Test继承自Object,并实现了 Serializable 和 Cloneable 这两个接口

    6.字段表

    • 紧跟在接口索引集合后面的就是字段表
      • 字段表的主要功能是用来描述类或者接口中声明的变量
      • 这里的字段包含了类级别变量以及实例变量,但不包含方法内部声明的局部变量
      • 一个类中的变量个数是不固定的,因此在字段表集合之前还是使用一个计数器来表示变量的个数
    11.字段表.png
    • 0001表示类中声明了1个变量,所以字段计数器之后会紧跟着1个字段表的数据结构

    字段表的具体结构为:

    CONSTANT_Fieldref_info{
        u2  access_flags        字段的访问标志
        u2  name_index          字段的名称索引(也就是变量名)
        u2  descriptor_index    字段的描述索引(也就是变量的类型)
        u2  attributes_count    属性计数器
        attribute_info
    }
    
    字段访问标志
    11-1字节码-字段访问标志.png

    Java类中的变量,也可以使用public,private,final,static等标识符进行标识。因此解析字段时,需要先判断它的访问标志,字段的访问标志如下:

    字段访问标志 描述
    ACC_PUBLIC 0X0001 字段是否为public
    ACC_PRIVATE 0X0002 字段是否为private
    ACC_PROTECTED 0X0004 字段是否为protected
    ACC_STATIC 0X0008 字段是否为static
    ACC_FINAL 0X0010 字段是否为final
    ACC_VOLATILE 0X0040 字段是否为volatile
    ACC_TRANSIENT 0X0080 字段是否为transient
    ACC_ENUM 0X4000 字段是否为enum

    字段表结构图中的访问标志值为0002,代表它是private类型,变量名索引指向第7的常量;变量名类型索引指向第8个字段。根据class字节码文件内容可以知道,

    11-1字节码-字段访问标志.png
    • 类中有一个名为num,类型为int类型的变量

    7.方法表

    字段表之后跟着的就是方法表常量。方法表常量应该也是以一个计数器开始的,因为一个类中的方法数量是不固定的。


    12-2.add方法表内容.png 12.方法表.png
    • 上图表示Test.class中有两个方法,我们声明了一个add方法,再加默认构造函数也被包含在方法表常量中
    • 方法表的结构如下:
    CONSTANT_Methodref_info{
        u2  access_flags;        方法的访问标志
        u2  name_index;          指向方法名的索引
        u2  descriptor_index;    指向方法类型的索引
        u2  attributes_count;    方法属性计数器
        attribute_info attributes;
    }
    
    • 方法的访问标志如下:
    字段访问标志 描述
    ACC_PUBLIC 0X0001 方法是否为public
    ACC_PRIVATE 0X0002 方法是否为private
    ACC_PROTECTED 0X0004 方法是否为protected
    ACC_STATIC 0X0008 方法是否为static
    ACC_FINAL 0X0010 方法是否为final
    ACC_SYNCHRONIZED 0X0020 方法是否被synchronized修饰
    ACC_VARARGS 0X0080 方法是否接收参数
    ACC_NATIVE 0X0100 方法是否为native
    ACC_ABSTRACT 0X4000 方法是否为abstract
    • 主要看add方法
    12-2.add方法表内容.png 12-3.add方法字节码文件内容.png
    • 从图中可知add方法的具体信息
      • access_flags = 0001 表示方法访问标志是public方法
      • name_index = 000D 方法名指向常量池第13个位置,也就是add
      • descriptor_index = 000E 方法类型指向常量池第14个位置,也就是(I)I,表示该方法接收int类型参数,并返回int类型出参

    8.属性表

    • 在前面解析字段和方法的时候,在他们的结构中都有看到有一个叫做attributes_count的表,这就是属性表

    属性表并没有一个固定的结构,各种不同的属性只要满足以下结构即可:

    CONSTANT_Attribute_info{
        u2 name_index;
        u2 attribute_length length;
        u1[] info;
    }
    
    • Code属性表

    继续查看刚才add方法中属性表内容:

    13-1.属性表内容.png
    • 可以看到add方法类型索引后跟着的就是add方法的属性
      • 0001 是属性计数器,代表只有一个属性
      • 000B 是属性表类型索引,指向常量池中第11个常量位置,是一个Code属性表,如下:
    13-2.add方法属性表类型.png
    13-3.add方法字节码Code属性表内容.png
    • Code属性表中,主要是一系列的字节码,JVM执行add方法时,就通过这一系列指令来做相应的操作
    总结
    • 字节码由javac编译器编译生成,然叫交给java虚拟机进行解析执行
    • 字节码主要由无符号数和表组成
    • class字节码文件内容包括
      • 文件标志魔数
      • 版本号
      • 常量池(重点)
      • 类的访问标志
      • 类索引,父类,接口索引
      • 字段表
      • 方法标志
      • 属性表

    相关文章

      网友评论

        本文标题:class(一) 字节码文件结构

        本文链接:https://www.haomeiwen.com/subject/ylhyiktx.html