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

Java中String字符的长度有限制吗?

这是面试中有可能会被问到的问题,对于这个问题答案,和class文件有关。

class文件

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

所以不管是什么Java虚拟机语言,最终都需要通过编译器将源代码编译成class文件。这样也就解除了多种语言与Java虚拟机之间的耦合。

多种语言最终都将编译为class字节码文件

而通过整体角度看,class文件里只有两种数据结构:无符号数

无符号数

无符号数属于基本的数据类型。以u1、u2、u4、u8来分别代表一个字、2个字节、4个字节和8个字节的无符号数。

无符号数可以用来描述数字、索引引用、数量值或者字符串(UTF-8编码)

有多个无符号或者其他表作为数据项构成复合数据类型。

class文件中的所有表都以“_info”结尾。

整个class文件本质就是一张表。

表与无符号之间的关系

class文件结构

class文件结构由无符号数和表组成,并且结构按照预先规定好的顺序进行紧密的排列。并且都是有16进制字节码构成。

class文件结构

当JVM加载class文件时,就是按照上图顺序进行解析,并进行相应的内存分配,分配大小如下图。

各结构所占空间大小

实例分析

通过对文件结构了解,结合实例进行分析。如下Java实例代码

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 文件,显示内容如下所示:


魔数

在class文件开头的四个字节是class文件的魔数,它是一个固定值 --0XCAFEBABE
魔数是class文件标志,JVM虚拟机通过开头四个字节的魔数值来判断是否是class文件。如果不是这个值,则表示当前不是class文件,不能被JVM识别和加载。

魔数

版本号

魔数后面思维则是版本号,而这个版本号对应的是jdk的版本号。


版本号

前两个字节0000代表次版本号,后两个字节0034是主版本号,对应的十进制数为52。当前class文件住版本号为52,此版本为0,所以中和版本号是52.0,也就是jdk1.8.0。

常量池

版本号后面是一个叫做常量池的表(cp_info),在常量池中报春了类各种相关信息。比如类的名称、父类的名称、类中的方法名、参数名称、参数类型等。

常量池中项目类型
常量池中的每一项都是一个表,其项目类型共有 14 种。并且每一项都会有一个 u1 大小的标识位值,而通过这个标识位值来判断,是具体那个表。

常量池表分析

CONSTANT_Class_info 表具体结构如下所示:

table CONSTANT_Class_info {
    u1  tag = 7;
    u2  name_index;
}

tag占用一个字节大小,上面tag=7,说明是CONSTANT_Class_info类型表,也就是类/接口的引用表。
而name_index则是一个索引值,可以理解为一个指针,指向常量池中索引为name_index的常量池表。比如name_index=2,则它指向常量池第二个常量。

接下来再看 CONSTANT_Utf8_info 表具体结构如下:

table CONSTANT_utf8_info {
    u1  tag=1;
    u2  length;
    u1[] bytes;
}

tag=1,表示是CONSTANT_Utf8_info类型表,length表示下面u1[]数组的长度。

如果在Java代码中声明的是String字符串,最终class文件中的存储格式则是CONSTANT_utf8_info。因此一个字符串最大长度也就是u2所能代表的最大值,也就是65536个。去掉保持null值的两个,最终String一个字符串最大长度为65536-2=65534

在常量池内部的表中也有相互之间的引用。用一张图来理解 CONSTANT_Class_info 和 CONSTANT_utf8_info 表格之间的关系,如下图所示:
name_index=7对应CONSTANT_utf8_info表中第七个常量,也就是Hello

结合上面的分析,就能解析版本号后面的常量池信息:

常量池大小

class文件在常量池的前面使用2个字节的容器计数器,用来代表当前类中的常量池大小。001d转化为十进制为29,也就是常量计数器值为29,其中下表为0的常量被JVM用作与其他特殊用途,因此Test.class中时机的常量池大小为29-1=28

第一个常量,如下所示:


image.png

0a 转化为 10 进制后为 10,通过查看常量池 14 种表格图中,可以查到 tag=10 的表类型为 CONSTANT_Methodref_info,因此常量池中的第一个常量类型为方法引用表。其结构如下:

CONSTANT_Methodref_info {
    u1 tag = 10;
    u2 class_index;        指向此方法的所属类
    u2 name_type_index;    指向此方法的名称和类型
}

在“0a”之后的 2 个字节指向这个方法是属于哪个类,紧接的 2 个字节指向这个方法的名称和类型。它们的值分别是:

  • 0006:十进制 6,表示指向常量池中的第 6 个常量。
  • 0015:十进制 21,表示指向常量池中的第 21 个常量。

另外也可以通过javap命令查看class常量池中的内容
通过javap命令查看class文件结构

访问标志

紧跟着常量池之后的是访问标志,占两个字节。


访问标志代表类或者借口的访问信息。比如该class文件是类还是接口,是否被定义为public,是否是abstract。如果是类,是否被声明成final等。

各项访问标志

Test.java 是一个普通 Java 类,不是接口、枚举或注解。并且被 public 修饰但没有被声明为 final 和 abstract,因此它所对应的 access_flags 为 0021(0X0001 和 0X0020 相结合)。

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

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


可以看出类索引指向常量池中的第 5 个常量,父类索引指向常量池中的第 6 个常量,并且实现的接口个数为 2 个。再回顾下常量池中的数据:

从图中可以看出,第 5 个常量和第 6 个常量均为 CONSTANT_Class_info 表类型,并且代表的类分别是“Test”和“Object”。再看接口计数器,因为接口计数器的值是 2,代表这个类实现了 2 个接口。查看在接口计数器之后的 4 个字节分别为:

  • 0007:指向常量池中的第 7 个常量,从图中可以看出第 7 个常量值为"Serializable"。
  • 0008:指向常量池中的第 8 个常量,从图中可以看出第 8 个常量值为"Cloneable"。

综上所述,可以得出如下结论:当前类为 Test 继承自 Object 类,并实现了“Serializable”和“Cloneable”这两个接口。

字段表

紧跟在接口索引集合后面的就是字段表了,字段表的主要功能是用来描述类或者接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量

同样, 一个类中的变量个数是不固定的,因此在字段表集合之前还是使用一个计数器来表示变量的个数,如下所示:



0002 表示类中声明了 2 个变量(在 class 文件中叫字段),字段计数器之后会紧跟着 2 个字段表的数据结构。

字段表的具体结构如下:

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

字段访问标识

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


字段访问标识

字段表结构图中的访问标志的值为 0002,代表它是 private 类型。变量名索引指向常量池中的第 9 个常量,变量名类型索引指向常量池中第 10 个常量。第 9 和第 10 个常量分别为“num”和“I”,如下所示:


因此可以得知类中有一个名为 num,类型为 int 类型的变量。

注意事项:

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

方法表

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


上图表示 Test.class 中有两个方法, Test.java 中声明了一个 add 方法,另外一个是默认构造器方法

方法表的结构如下所示:

CONSTANT_Methodref_info{
    u2  access_flags;        方法的访问标志
    u2  name_index;          指向方法名的索引
    u2  descriptor_index;    指向方法类型的索引
    u2  attributes_count;    方法属性计数器
    attribute_info attributes;
}

方法也是有自己的访问标志,具体如下:


方法访问标识

add 方法,具体如下:


add 方法的以下字段的具体值:

  1. access_flags = 0001 也就是访问权限为 public。
  2. name_index = 0X0011 指向常量池中的第 17 个常量,也就是“add”。
  3. type_index = 0X0012 指向常量池中的第 18 个常量,也即是 (I)。这个方法接收 int 类型参数,并返回 int 类型参数。

属性表

在之前解析字段和方法的时候,在它们的具体结构中我们都能看到有一个叫作 attributes_info 的表,就是属性表。

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

CONSTANT_Attribute_info{
    u2 name_index;
    u2 attribute_length length;
    u1[] info;
}

Code属性表


可以看到,在方法类型索引之后跟着的就是“add”方法的属性。0X0001 是属性计数器,代表只有一个属性。0X000f 是属性表类型索引,通过查看常量池可以看出它是一个 Code 属性表,如下所示:


Code 属性表中,最主要的就是一些列的字节码。通过 javap -v Test.class 之后,可以看到方法的字节码,如下图显示的是 add 方法的字节码指令:


JVM 执行 add 方法时,就通过这一系列指令来做相应的操作。

你可能感兴趣的:(字节码层面分析class类文件结构课程)