美文网首页美文共赏
字节码层面分析class类文件结构

字节码层面分析class类文件结构

作者: 沅兮 | 来源:发表于2021-12-06 14:37 被阅读0次

    一个面试题:Java 中 String 字符串的长度有限制么?

    【答案】String 的长度是有限制的。

    • 编译器的限制:字符串的 UTF-8 编码值的字节数不能超过 65535,字符串的长度不能超过 65534
    • 运行时的限制:字符串的长度不能超过 2^31-1,占用的内存数不能超过虚拟机能够提供的最大值。
    • 长度为 2^31-1 的字符串所占用的空间大小为:4G。

    Java 提供了一种在所有平台上都能使用的一种中间代码--字节码类文件(*.class文件)

    • 有了字节码,无论哪种平台只要安装了虚拟机都可以直接运行字节码
    • 有了字节码,解除了 Java 虚拟机和 Java 语言之间的耦合

    一、Class中的数据结构

    从纵观角度看,class 文件里只有两种数据结构:无符号数

    【无符号数】

    • 属于基本的数据类型。
    • 以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。
    • 无符号数可以用来描述数字、索引引用、数量值或字符串(UTF-8编码)。

    【表】

    • 表是有多个无符号数或其他表作为数据项构成的复合数据类型。
    • class 文件中所有的表都以 “_info” 结尾。
    • 整个 Class 文件本质上就是一张表。

    表和无符号之间的关系图

    表和无符号之间的关系

    可用下面的伪代码表示

    // 无符号数
    byte[] u1 = new byte[1];
    byte[] u2 = new byte[2];
    byte[] u4 = new byte[4];
    byte[] u8 = new byte[8];
    
    // 表
    class _table {
        // 表中可以引用无符号数
        u1 tag;
        u4 index;
        
        // 表中可以引用其他表
        method_info table;
    }
    

    二、Class文件结构

    无符号数和表组成了 class 中的各个结构。

    这些结构按照 预先规定好的顺序 紧密的从前向后排列,相邻的项之间没有任何间隙。

    class 文件结构如下

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

    当 JVM 加载某个 class 文件时,JVM 就是根据上图的结构进行解析 class 文件到内存中,并在内存中分配相应的空间。

    每种结构所占用的空间大小如下表:

    字段 名称 数据类型 数量
    magic number 魔数 u4 1
    major version 主版本号 u2 1
    minor version 副版本号 u2 1
    constant_pool_count 常量池大小 u2 1
    constant_pool 常量池 cp_info countant_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

    示例

    public class ClassHexNormal implements Serializable, Cloneable {
    
        private int num = 1;
    
        public int add(int i) {
            int j = 10;
            num = num + i;
            return num;
        }
    }
    

    将上述代码编译成 .class 文件,使用 16 进制编辑器打开:

    16进制字节码文件

    下面我们通过上图来一步步解析字节码文件:

    1、魔数 magic numebr
    魔数

    在 class 文件开头的四个字节是 class 文件的魔数,它是一个固定值 0XCAFEBABE

    魔数是 class 文件的标志,它是判断一个文件是不是 class 格式文件的标准。

    2、版本号
    版本号

    前两个字节 0000 代表 次版本号 minor_version。后两个字节 0034 是 主版本号 major_version,对应的十进制值为 52。

    所以当前 class 文件的主版本号为 52,次版本号为 0,所以综合版本号是 52.0,也就是 jdk1.8.0。

    3、常量池(重点)

    紧跟在版本号之后的是一个叫做 常量池的表 cp_info,在常量池中保存了类的各种相关信息。比如类的名称、父类的名称、类中的方法名、参数名称、参数类型等。

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

    表名 标识位 描述
    CONSTANT_uft8_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 动态方法调用表

    以 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_Uft8_info 表为例:

    table CONSTANT_uft8_info {
        u1 tag;
        u2 length;
        u1[] bytes;
    }
    

    【tag】值为 1,表示 CONSTANT_Utf8_info 类型表。

    【length】表示 u1[] 的长度,比如 length=5,则表示接下来的数据是 5 个连续的 u1 类型数据。

    【bytes】u1 类型数组,长度为上面第 2 个参数 length 的值。

    【注意】

    在 java 代码中声明的 String 字符串最终在 class 文件中的存储格式是 CONSTANT_utf8_info。因此一个字符串最大长度也就是 u2 所能表达的最大值 65536 个。但是需要使用2个字节来保存 null 值,因此一个字符串的最大长度为 65536-2 = 65534。

    常量池内部的表中也有表与表之间的相互引用,如下图:

    表与表之间的关系

    16进制中的常量池大小

    常量池

    class 文件在常量池的前面使用 2个字节 的容量计数器,用来代表当前类中常量池的大小。

    上图中 0017 转化为十进制是 29,也就是说常量计数器的值为 23。其中下标为 0 的常量被 JVM 留作其他特殊用途,因此当前 class 中实际的常量池大小为这个计数器的值减 1,也就是 22 个。

    常量池第一个常量

    常量池第一个参数

    上图中 0A 转化为10进制后为 10。说明对应常量池 14 种表格图中的 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 个常量。

    这里只解析了第一个常量,后面还有 21 个常量,也是与上面说的类似,第一个参数对应 14 种类型的下标,之后再看具体的表结构;如果u1表示一个字节,u2 表示后面 2 个字节,以此类推。

    借助 javap 命令

    我们可以借助 javap 命令查看 class 常量池中的内容:

    javap -v Test.class
    
    // 借助命令查看22个常量
    Constant pool:
        // 下标为1的指向 下标为4 和 下标为 17的常量
       #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
       // 下标为2的指向下标为3和18的常量
       #2 = Fieldref           #3.#18         // ClassHexNormal.num:I
       #3 = Class              #19            // ClassHexNormal
       #4 = Class              #20            // java/lang/Object
       #5 = Class              #21            // java/io/Serializable
       #6 = Class              #22            // java/lang/Cloneable
       #7 = Utf8               num
       #8 = Utf8               I
       #9 = Utf8               <init>
      #10 = Utf8               ()V
      #11 = Utf8               Code
      #12 = Utf8               LineNumberTable
      #13 = Utf8               add
      #14 = Utf8               (I)I
      #15 = Utf8               SourceFile
      #16 = Utf8               ClassHexNormal.java
      #17 = NameAndType        #9:#10         // "<init>":()V
      #18 = NameAndType        #7:#8          // num:I
      #19 = Utf8               ClassHexNormal
      #20 = Utf8               java/lang/Object
      #21 = Utf8               java/io/Serializable
      #22 = Utf8               java/lang/Cloneable
    

    有上可知,下标为 1 的常量表示 Object()方法。

    4、访问标志 access_flags

    紧跟在常量池之后的常量时访问标志,占用两个字节。访问标志代表类或接口的访问信息

    比如:该 class 文件是类还是接口,是否被定义成 public,是否是 abstract,如果是了是否被声明成 final 等。

    访问标志如下:

    访问标志 描述
    ACC_PUBLIC 0x0001 public类型
    ACC_FINAL 0x0010 被声明为final类型的类
    ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真
    ACC_INTERFACE 0x0200 标志这是一个接口类型
    ACC_ABSTRACT 0x0400 抽象类或接口类型
    ACC_ANNOTATION 0x2000 注解
    ACC_ENUM 0x4000 枚举

    上面定义的类 ClassHexNormal.java 是一个普通 Java 类,不是接口、枚举、注解。并且被 public 修饰,但没有被声明为 final 和 abstract,因此它对应的 access_flags 为 0021 (0x0001和0x0020结合)

    5、类索引、父类索引、接口索引计数器

    标志后的2个字节是 类索引;类索引后的2个字节是 父类索引;父类索引后的2个字节是 接口索引计数器

    6、字段表

    紧跟在接口索引结合后面的就是字段表;字段表的主要功能是用来 描述类或接口中声明的变量

    这里的字段包含类级别变量以及实例变量,不包括方法内部声明的局部变量。

    【注意事项】

    1. 字段表集合中不会列出从父类或者父接口中继承而来的字段。
    2. 内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
    7、方法表

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

    后面数据依次类推,这里不再举例说明。

    相关文章

      网友评论

        本文标题:字节码层面分析class类文件结构

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