美文共赏

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

2021-12-06  本文已影响0人  沅兮

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

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

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

一、Class中的数据结构

从纵观角度看,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、方法表

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

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

上一篇下一篇

猜你喜欢

热点阅读