什么是.class

      Java源文件被编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式成为class文件格式。class文件格式中精确地定义了类与接口的表示形式。
      每个class文件都由字节流组成,每个字节含有8个二进制位,所有16位,32位和64位长度的数据将通过构造2个、4个和8个连续的8位字节来表示。在Java SDK中,可以使用java.io.DataInput、java.io.DataOutput等接口和java.io.DataInputStream和java.io.DataOutputStream等类来访问这种格式的数据。

ClassFile结构

      每个class文件对应一个如下所示的ClassFile结构:

ClassFile {
    u4                magic;
    u2                minor_version;
    u2                major_version;
    u2                constant_pool_count;
    cp_info           constant_pool[constant_pool_count-1];
    u2                access_flags;
    u2                this_class;
    u2                super_class;
    u2                interfaces_count;
    u2                interfaces[interfaces_count];
    u2                fields_count;
    field_info        fields[fields_count];
    u2                methods_count;
    method_info       methods[methods_count];
    u2                attributes_count;
    attribute_info    attributes[attributes_count];
}

      JVM规范专门定义了一组专用的数据类型来表示class文件的内容,它们包括u1、u2和u4,分别代表1、2和4个字节的无符号数。对于其他类型如cp_info、field_info等都是复合结构,介绍到再详细讲。

准备

      在本次分析字节码的过程中以下代码将一直陪伴我们,我们将分析它的class文件:

package bytecode;

public class Test1 {
    private int a = 1;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

      class文件是16进制的数据,需要使用WinHex打开,先下载安装吧!打开class文件如下所示(看不懂没关系,有规则的):
Test1.class文件

      为了判断我们的分析结果我们可以使用javap -verbose <filename>命令让Java告诉我们class文件的详细信息。

魔数

      所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE。它的作用是确定这个文件是否为一个能被虚拟机所接受的class文件。

版本号

      继魔数之后的4个字节是版本信息,其中前2个字节为minor_version(副版本号),后2个字节为(主版本号)。如这次使用的Test1.class文件中的版本信息为00 00 00 34,转换为十进制则副版本号为0,主版本号为52。

常量池计数器

      紧接着版本号的2个字节为constant_pool_count(常量池计数器),此数值表示常量池中成员的数目。如这次使用的Test1.class文件中的版本信息为00 18,转换为十进制为24,说明常量池数目为24个。但是,我们查看javap给我们的信息可以发现实际上常量池数目为23。
常量池数目

      注意:常量池数组中元素的个数 = 常量池数 - 1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值,所以常量池的索引从1而非0开始。

常量池

      一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java中声明为final的常量值等,而符号引用如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符等。
      常量池中的每一项都具备相同的特征——第1个字节作为类型标记,用于确定该项的格式,这个字节成为tag byte(标记字节、标签字节)
      常量池表中的所有项都具有如下通用格式。

cp_info {
    u1    tag;
    u1    info[];
}

      在常量池表中,每个cp_info项都必须表示cp_info类型的单字节"tag"项开头。后面info[]数组的内容由tag的值所决定。有效的tag和对应的值如下表所示。每个tag字节之后必须有两个或更多的字节,这些字节用于指定这个常量的信息,附加信息的格式由tag的值来决定。

名称 项目 类型 描述
CONSTANT_Utf8_info
tag u1 值为1
length u2 UTF-8编码的字符串长度
bytes u1 长度为length的UTF-8编码的字符串
CONSTANT_Integer_info
tag u1 值为3
bytes u4 按照高位在前存储的int值
CONSTANT_Float_info
tag u1 值为4
bytes u4 按照高位在前存储的float值
CONSTANT_Float_info
tag u1 值为5
bytes u8 按照高位在前存储的long值
CONSTANT_Double_info
tag u1 值为6
bytes u8 按照高位在前存储的double值
CONSTANT_Class_info
tag u1 值为7
index u2 指向全限定名常量项的索引。
CONSTANT_String_info
tag u1 值为8
index u2 指向字符串字面量的索引。
CONSTANT_Fieldref_info
tag u1 值为9
index u2 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
index u2 指向字段描述符CONSTANT_NameAndType_info的索引项
CONSTANT_Methodref_info
tag u1 值为10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_InterfaceMethodref_info
tag u1 值为11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_NameAndType_info
tag u1 值为12
index u2 指向改字段或方法名称常量项的索引
index u2 指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_info
tag u1 值为15
kind u1 值必须在1~9之间,表示方法句柄的类型
index u2 指向该方法 常量项的索引
CONSTANT_MethodType_info
tag u1 值为16
index u2 指向该方法类型描述符常量项的索引
CONSTANT_InvokeDynamic_info
tag u1 值为18
index u2 对当前class文件中引导方法表的bootstrap_methods数组的有效索引
index u2 对常量项的索引,必须为CONSTANT_NameAndType_info结构

      上表记不住?没关系,不必要去记,会查就行。看不懂,没关系,马上教!

      继Constant_pool_count后就是常量池了,我们知道常量池中的每一项都具备相同的特征——第1个字节作为类型标记。查看Test1.class字节码,tag为0x0A,十进制为10,查表得类型为CONSTANT_Methodref_info,该类型除tag外还有两个u2类型的index,所以接着数4个字节,前2个字节(0x0004表示4),后2个字节(0x0014表示20),OK!我们得到了第一个常量:#1=Methodref #4.#20。由于我们还没分析完所以暂时不知道引用的具体常量项含义。
#2=Fieldref   #3.#21

#3=Class    #22

#4=Class    #23

      接下来的类型是Constant_Utf8,除tag外length和bytes分别代表不同含义,length表示给Utf8字符串的占多少个字节,比如占5个字节,那么我们往后数5个字节的数据就是该字符串表示的具体内容了(就是bytes)。
#4=Class    #23

      按照上述方法分析出索引5~19之间的Utf8类型的数据如下:

#5 = Utf8            a
#6 = Utf8            I
#7 = Utf8            <init>
#8 = Utf8            ()V
#9 = Utf8            Code
#10= Utf8            LineNumberTable
#11= Utf8            LocalVariableTable
#12= Utf8            this
#13= Utf8            Lbytecode/Test1;
#14= Utf8            getA
#15= Utf8            ()I
#16= Utf8            setA
#17= Utf8            (I)V
#18= Utf8            SourceFile
#19= Utf8            Test1.java

5~19Utf8

#20= NameAndType    #7.#8

#21= NameAndType    #5.#6

      最后的两个常量项都是Utf8类型,值为:

#22= Utf8           bytecode/Test1
#23= Utf8           java/lang/Object

      至此常量池中的所有项都分析完了,我们可以根据引用的索引项写上注释,结果如下所示:

Constant_pool:
    #1 = Methodref       #4.#20                 //java/lang/Object.<init>:()V
    #2 = Fieldref        #3.#21                 //bytecode/Test1.a:I
    #3 = Class           #22                    //bytecode/Test1
    #4 = Class           #23                    //java/lang/Object
    #5 = Utf8            a
    #6 = Utf8            I
    #7 = Utf8            <init>
    #8 = Utf8            ()V
    #9 = Utf8            Code
    #10= Utf8            LineNumberTable
    #11= Utf8            LocalVariableTable
    #12= Utf8            this
    #13= Utf8            Lbytecode/Test1;
    #14= Utf8            getA
    #15= Utf8            ()I
    #16= Utf8            setA
    #17= Utf8            (I)V
    #18= Utf8            SourceFile
    #19= Utf8            Test1.java
    #20= NameAndType     #7:#8                  //<init>:()V    
    #21= NameAndType     #5:#6                  //a:I
    #22= Utf8            bytecode/Test1
    #23= Utf8            java/lang/Object

      也许您会疑惑诸如()V,Lbytecode/Test1;等书写方式,读完下面这三段就知道了:

  1. 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示,B - byte,C - char,D - double,F - float,I - int,J - long,S - short,Z - boolean,V - void,L - 对象类型,如Ljava/lang/String;
  2. 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为[I,String[][]被记录为[[Ljava/lang/String;
  3. 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法String
    setNameAndAge(String name,int
    age)的描述符为:(Ljava/lang/String;,I)Ljava/lang/String;

      对比javap命令分析的结果,完美!OK,分析常量池就结束了,接下来的文章将分析余下的结构。

Last modification:February 4th, 2020 at 04:19 pm
如果觉得我的文章对你有用,请随意赞赏