zoukankan      html  css  js  c++  java
  • Class类文件结构

    引言

      Class文件格式采用伪结构来存储数据,该伪结构有两种数据类型:无符号数和表。

      无符号数是基本数据类型,以u1、u2、u4、u8表示1个字节、2个字节、4个字节、8个字节的无符号数。无符号数用来描述数字、索引引用、数量值或按UTF-8编码构成的字符串值。

      表是由多个无符号数或其他表作为数据项构成的复合数据类型。整个Class文件本质上就是一个表。

    类型 名称 数量
    u4 magic(魔数) 1
    u2 minor_version(次版本号) 1
    u2 major_version(主版本号) 1
    u2 constant_pool_count(常量池中常量的数量) 1
    cp_info constant_pool(常量池) constant_pool_count-1
    u2 access_flags(访问标志) 1
    u2  this_class(类索引) 
    u2  super_class(父类索引)  1
    u2  interfaces_count(类实现接口数) 
    u2 interfaces(类实现的接口) interfaces_count
    u2 fields_count(字段数量) 1
    field_info fields(字段表) fields_count
    u2 methods_count(方法数量) 1
    method_info methods(方法表) methods_count
    u2 attributes_count(属性数量) 1
    attribute_info attributes(属性表) attributes_count
    package com.wjz.clazz;
    
    public class TestClass {
        private int m;
        public int inc() {
            return m + 1;
        }
    }

    使用16进制的WinHex工具打开其对应的Class文件

     图1

    魔数和Class文件版本号

      每个Class文件的头4个字节称为魔数(Magic Number),他的唯一作用是判断该文件是否能为JVM识别的Class文件。值为0xCAFEBABE。

      紧接着魔数之后的4个字节是Class文件版本号,5和6字节是次版本号(Minor Version),7和8字节是主版本号(Major Version)。JVM可运行低版本的Class文件,但是无法运行高版本的Class文件。

    常量池

    常量池容量

      紧接着Class文件版本号之后时常量池,常量池中的数量是不固定的,所以入口处有一个u2类型的数据代表常量池容量计数值(constant_pool_count)。

           图2

      常量池容量为十六进制数0x0013即19,代表常量池中有18个常量(第0项常量空出),索引值范围为1~18(计数从1开始)。

    常量池

      常量池中主要存放两大类常量:字面量和符号引用。字面量如字符串、声明为final的常量值等。符号引用主要有三种常量:类或接口的全限定名、字段的名称和描述符、方法的名称和描述符。

      常量池中每一项常量都是一个表。共同点是表开始的第一位是标志位(tag)代表当前常量属于哪种常量类型。

      

      

      

                            图3

      回头看图1中常量池的第一个常量的标志位为十六进制数0x0A即10,表明常量池的第一个常量类型为CONSTANT_Methodref_info(类中方法的符号引用)。

                        CONSTANT_Methodref_info型常量的结构

      

      第一个常量为0x0A(tag)、0x0004(CONSTANT_Class_info的索引项,指向常量池中的第4个常量)、0x000F(CONSTANT_NameAndType的索引项,指向常量池中的第15个常量)

      

       紧接着下一个常量的标志位为十六进制数0x09即9,代表着该常量类型为CONSTANT_Fieldref_info(字段的符号引用)。

                         CONSTANT_Fieldref_info型常量的结构

      

      第二个常量为0x09(tag)、0x0003(CONSTANT_Class_info的索引项,指向常量池中的第3个常量)、0x000F(CONSTANT_NameAndType的索引项,指向常量池中的第16个常量)

      

      紧接着下一个常量的标志位为十六进制数0x07即7,代表着该常量类型为CONSTANT_Class_info(类或接口的符号引用)。

                       CONSTANT_Class_info型常量的结构

      

      第三个常量为0x07(tag),0x0011(CONSTANT_Utf8_info的索引项,指向常量池中的第17个常量)

      

      紧接着下一个常量的标志位为十六进制数0x07即7,代表着该常量类型为CONSTANT_Class_info(类或接口的符号引用)。

      第四个常量为0x07(tag),0x0012(CONSTANT_Utf8_info的索引项,指向常量池中的第18个常量)

       

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

                      CONSTANT_Utf8_info型常量的结构

      

      第五个常量为0x01(tag)、0x0001(UTF编码的字符串占用的字节数即1个字节)、0x6D(ASCII码对应的字符 "m")

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

      第六个常量为0x01(tag)、0x0001(UTF编码的字符串占用的字节数即1个字节)、0x49(ASCII码对应的字符 "I")

       

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

      第七个常量为0x01(tag)、0x0006(UTF编码的字符串占用的字节数即6个字节)、0x3C(ASCII码对应的字符 "<")、0x69("i")、0x6E("n")、0x69("i")、0x74("t")、0x3E(">")

       

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

      第八个常量为0x01(tag)、0x0003(UTF编码的字符串占用的字节数即3个字节)、0x28(ASCII码对应的字符 "(")、0x29(")")、0x56("V")

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

       第九个常量为0x01(tag)、0x0004(UTF编码的字符串占用的字节数即4个字节)、0x43(ASCII码对应的字符 "C")、0x6F("o")、0x64("d")、0x65("e")

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

       第十个常量为0x01(tag)、0x000F(UTF编码的字符串占用的字节数即15个字节)、ASCII码对应的字符串为 "LineNumberTable"  

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

         第十一个常量为0x01(tag)、0x0003(UTF编码的字符串占用的字节数即3个字节)、ASCII码对应的字符串为 "inc"

       

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

         第十二个常量为0x01(tag)、0x0003(UTF编码的字符串占用的字节数即3个字节)、ASCII码对应的字符串为 "()I"

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

         第十三个常量为0x01(tag)、0x000A(UTF编码的字符串占用的字节数即10个字节)、ASCII码对应的字符串为 "SourceFile"

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

         第十四个常量为0x01(tag)、0x000E(UTF编码的字符串占用的字节数即14个字节)、ASCII码对应的字符串为 "TestClass.java"

       

      紧接着下一个常量的标志位为十六进制数0x0C即12,代表着该常量类型为CONSTANT_NameAndType_info(字段或方法的部分符号引用)。

                      CONSTANT_NameAndType_info型常量的结构

       

      第十五个常量为0x0C(tag)、0x0007(CONSTANT_Utf8_info的索引项,指向常量池中的第7个常量)、0x0008(CONSTANT_Utf8_info的索引项,指向常量池中的第8个常量)

       

      紧接着下一个常量的标志位为十六进制数0x0C即12,代表着该常量类型为CONSTANT_NameAndType_info(字段或方法的部分符号引用)。

      第十六个常量为0x0C(tag)、0x0005(CONSTANT_Utf8_info的索引项,指向常量池中的第5个常量)、0x0006(CONSTANT_Utf8_info的索引项,指向常量池中的第6个常量)

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

         第十七个常量为0x01(tag)、0x0017(UTF编码的字符串占用的字节数即23个字节)、ASCII码对应的字符串为 "com/wjz/clazz/TestClass"

      

      紧接着下一个常量的标志位为十六进制数0x01即1,代表着该常量类型为CONSTANT_Utf8_info(UTF8编码的字符串)。

         第十八个常量为0x01(tag)、0x0010(UTF编码的字符串占用的字节数即16个字节)、ASCII码对应的字符串为 "java/lang/Object"

      

      至此我们就将常量池的部分分析完了,当然我们还可以通过命令行的方式来分析和验证我们上述分析的结果。使用 javap 输出常量表

      

    访问标志

      常量池结束之后的两个字节代表访问标志(access_flags)。

      

      TestClass不是接口、注解、枚举类,被public关键字修饰没有声明为final或abstract,所以只有ACC_PUBLIC和ACC_SUPER为真,access_flags的值为0x0001 | 0x0200 = 0x0021。

      

    类索引、父类索引和接口索引集合

      类索引(this_class)、父类索引(super_class)都是一个u2类型的数据,接口索引集合是一组u2类型数据的集合,Class文件通过这三者来确定类的继承关系。

      类索引确定这个类的全限定名,父类索引确定该类的父类的全限定名,索引(全限定名)指向了常量池中的某一个常量。

      接口索引集合就是描述该类实现了哪些接口,对于接口索引的入口第一项是一个u2类型的容量计数器(interfaces_count),如果该类没有实现任何接口后面的接口索引集合(interfaces)不再占用任何字节。

      类索引为0x03,父类索引为0x04,接口索引集合为0x00(只有一个接口索引容量计数器)

      

    字段表集合

    字段表容量

      字段表的入口会有一个占用2个字节的容量计数器(field_count)用于记录类中的字段个数。

    字段表 

      字段表(field_info)用于描述类或接口中声明的变量。字段包括类级变量和实例级变量不包括方法中的局部变量。

      

      其中access_flags描述的是字段的修饰符,name_index和descriptor_index都是对常量池的引用分别代表简单名称和描述符(int类型字段是 'I',对象类型是 'L')。

      0x0001(fields_coount,代表有一个字段),0x0002(字段修饰符为private),0x0005(name_index,指向常量池中的第5个常量),0x0006(descriptor_index,指向常量池中的第6个常量),0x0000(attributes_count,属性表计数器)

      

      

      如果字段m的声明改为 “private static final int m = 10;”此时attributes_count不再会是0,attributes中也该多一项ConstantValue的属性。

      字段表不会列出父类或实现接口中的字段,但是会列出Java代码中不存在的字段如内部类会自动添加指向外部类实例的字段。

    方法表集合

      Class文件存储格式中对方法的描述和对字段的描述几乎一致。其中访问标志和属性表集合的可选项不同,并且方法中的代码放到了“Code”属性中。

      

      0x0002(方法表容量,说明有两个方法),0x0001(修饰符为public),0x0007(name_index,指向常量池中的第7个常量即“<init>”),0x0008(descriptor_index,指向常量池中的第8个常量即“()V”),0x0001(attributes_count,属性表计数器),0x0009(attributes类型,attribute_info属性表类型,指向常量池中的一个常量即“Code”)  

      

      方法表不会列出父类的方法信息,但会列出编译器自动添加的方法如类构造器“<clinit>”方法、实例构造器“<init>”方法和桥接方法等。

    属性表集合

    Code属性

      Java程序方法体中的代码经编译器编译为字节码后存储在Code属性中。属性表无严格的先后顺序,都是指向常量池的值。

      

      attribute_name_index是指向常量池的一个索引固定为“Code”值,attribute_length代表属性值长度(固定属性表长度减6个字节)。

      max_stack代表操作数栈深度的最大值,方法执行任何时刻操作数栈都不能超过该值,虚拟机运行时会根据该值分配栈帧的操作栈深度。

      max_locals代表局部变量表所需要的存储空间,单位是Slot(虚拟机为局部变量表分配内存时的最小单位)。方法参数(包括隐藏参数this)、显示异常处理器参数(try-catch中catch块定义的异常)、方法体中定义的局部变量都放在局部变量表中。

      code_length和code用来存储Java程序被编译后生成的字节码指令。code_length代表字节码指令的长度,code用来存储字节码指令的一系列字节流。虚拟机读取到code中的一条字节码指令时会对应找出该字节码指令对应的指令。一个字节码指令是1个字节即最多表达256条指令,虽然code_length是u4类型的但其实只使用了u2的长度即一个方法最多允许有216-1条指令。

      方法表集合中提到的<init>方法的attribute属性表中为下图所示,0x0009(常量池第9个常量)即Code属性,0x0000001D(属性值长度),0x0001(操作数栈最大深度)、0x0001(本地变量表容量)、0x00000005(字节码指令的长度)

      

      翻译" 2A  B7  00  01  B1 "的过程为:

      1)读入 0x2A,查询字节码指令表可知 0x2A 对应的指令为 aload_0,该指令的含义为将第0个Slot中为reference类型的本地变量推送至操作数栈顶。

      2)读入 0xB7,0xB7 对应的指令为 invokespecial,该指令的含义是调用栈顶reference类型指向的对象父类构造方法、实例构造方法或实例私有方法。紧接着会有一个u2类型的参数说明具体调用哪个方法,它指向常量池中为CONSTANT_Methodref_info的一个常量。

      3)读入 0x0001,这是invokespecial指令的参数指向常量池中第1个常量即实例构造器“<init>”方法的符号引用。

      4)读入 0xB1,0xB1对应的指令为 return,含义是返回此方法并且返回值类型为 void,该指令执行后当前方法结束。

      

      我们可以看到两个方法的locals和args_size均为1,然而源码中并未定义局部变量和方法参数,这是因为在实例方法的局部变量表中会预留出第一个Slot位来存放指向当前对象的引用。在任意实例方法中都可以通过this访问到该方法所属对象,该访问机制是编译器编译时会把对this的访问转变为对普通方法参数的访问,然后在虚拟机调用实例方法时自动传入一个方法参数即当前对象的引用。因此实例方法的局部变量表中至少存在一个指向当前对象的局部变量,方法形参列表中多一个当前对象参数,实例方法中的实际参数索引值从1开始计数,该访问机制只对实例方法有效,如果方法inc声明为static那么args_size的值则为0。 

      code字节码指令之后是方法的显示异常处理表集合,如果当字节码在start_pc到end_pc之间(不含后者)出现catch_type(指向常量池)或其子类的异常则转到handler_pc行处理,catch_type为0时,任意异常情况都转到handler_pc行处理。

      

      字节码层面的异常表

      

    Exception属性

       该属性的作用就是列出方法可能抛出的受检查异常(即方法throws后边的异常).

      

    LineNumberTable属性

      该属性用于描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系。

      

      line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号后者是Java源码行号。

      

    LocalVariableTypeTable属性

      该属性用于描述栈帧中局部变量表的变量和Java源码中定义的变量之间的的关系。

      

    SourceFile属性

      用于记录生成该Class文件的源文件名称。

      

    ConstantValue属性

      作用是通知虚拟机自动为静态变量赋值。非静态变量是在实例构造器函数<init>方法中被赋值的,静态变量的赋值有两种方式:在类构造器函数<clinit>方法和ConstantValue属性。当同时使用final和static修饰并且变量类型为基本数据类型或String类型则使用ConstantValue属性,否则在<clinit>中初始化。

      

    InnerClass属性

      用来记录内部类和宿主类之间的关联。如果一个类中定义了内部类,编译器会为该类和其包含的内部类生成InnerClass属性。

      

      每一个内部类信息都由一个inner_classes_info表描述。

      

    Deprecated和Synthetic属性

      Deprecated属性用于表示某个类、字段或方法不推荐使用了。

      Synthetic属性表示字段或方法不是Java源码直接产生,而是由编译器自动添加的。所有这个字段或方法都应至少设置Synthetic属性和ACC_SYNTHETIC属性,典型的是桥接方法,但不包括<init>和<clinit>。

      

    StackMapTable属性

      该属性位于Code属性的属性表中,该属性会在虚拟机类加载的字节码验证被新类型检查验证器使用。

      

    Signature属性

      用于记录泛型信息。Java的反射API能获得泛型信息最终也是通过该属性得到的。

      

    BootstrapMethods属性

      该属性用于保存invokedynamic指令引用的引导方法限定符。

  • 相关阅读:
    0031 Java学习笔记-梁勇著《Java语言程序设计-基础篇 第十版》英语单词
    0030 Java学习笔记-面向对象-垃圾回收、(强、软、弱、虚)引用
    0029 Java学习笔记-面向对象-枚举类
    0028 Java学习笔记-面向对象-Lambda表达式
    0027 Java学习笔记-面向对象-(非静态、静态、局部、匿名)内部类
    0026 Java学习笔记-面向对象-抽象类、接口
    0025 Java学习笔记-面向对象-final修饰符、不可变类
    0024 Java学习笔记-面向对象-包装类、对象的比较、String常量池问题
    0023 Java学习笔记-面向对象-初始化代码块
    0022 Java学习笔记-面向对象-继承、多态、组合
  • 原文地址:https://www.cnblogs.com/BINGJJFLY/p/7649572.html
Copyright © 2011-2022 走看看