Java字节码:深度分析Class类文件


title: Java字节码(一):深度分析Class类文件
date: 2019-03-27 15:58:04
categories:

  • Java虚拟机
    tags:
  • Java字节码

引言

我们知道,java是一个跟平台无关性的编程语言,而平台无关性的基础就是虚拟机与字节码存储格式。Java虚拟机不和包括java语言在内的任何语言绑定,它只认Class文件(kotlinscala等皆可在jvm上运行)。Class文件中包含了一个Java程序的指令集和符号集以及其他信息,编译器严格按照规范来将Java程序编译为字节码。在这儿,我们分析下Class文件的数据结构。

Class类文件结构

Class文件是以8位字节为基础的二进制流文件,其中没有间隔,在文件中只保留了必要的数据,节省了大量的空间。在Class字节码中有两种数据类型:

  • 字节数据直接量:这是基本的数据类型。共细分为u1u2u4u8四种,分别代表连续的1个字节、4个字节、8个字节组成的整体数据。
  • 表(数组):表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。

如下图所示,Class文件中字节码按一下顺序进行排列。

Java字节码:深度分析Class类文件

我们通过最简单的一个程序,来示例讲解Class文件结构。

public class MyTest1 {

    private int a = 1;

    public int getA() {
        return a;
    }

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


}

使用16进制工具(winHex)打开MyTest1.class文件,16进制字节码中,一个数字或字母占4位,两个数字或字母代表一个字节。

Java字节码:深度分析Class类文件

使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的构造方法、类中的方法信息、类变量与成员变量等信息。

Java字节码:深度分析Class类文件

魔数(4个字节)

所有的.class字节码文件的前4个字节都是魔数,魔数值是一固定值:0xCAFEBABE。 是用以Java虚拟机确定class文件的标志。很多文件格式都采用了魔数来进行文件的身份标识,比如jpg。选择在文件内容头部使用魔数而不用扩展名来进行文件的身份标识,主要是考虑了安全问题,因为扩展名易被修改。而魔数则可以由文件格式的制定者随意的指定,只要选择的魔数没有被广泛采用且不与其他魔数重复引起混淆就行。

版本号(2+2个字节)

.魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)。这里00 00 00 34,换算成十进制,表示次版本号为0,主版本号52。表示java版本1.8.01.8表示主版本号,0表示次版本号。可以使用java -version查看。JDK是向下兼容的,高版本的JDK能运行低于此版本的Class文件,低版本的JDK无法运行高于它本身的Class文件,即使文件格式未出错,虚拟机也拒绝执行高于其版本的Class文件。若进行运行,会报错java.lang.UnsupportedClassVersionError

常量池(2+n个字节)

常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。

常量池中主要存储两类常量:字面量和符号引用。

  • 字面量如文本字符串,Java中声明为final的常量值等。
  • 符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。
常量池的总体结构

Java类所对应的常量池主要由常量池与常量池数组(常量表)这两部分共同构成。

  • 常量池数量紧跟在主版本号后面,占据2个字节

  • 常量池数组则紧跟在常量池数量之后。

    常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同,这些元素是被称之为表的数据结构;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据一个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。

    值得注意的是,常量池数组中元素的个数 = 常量池数量 - 1 (其中0位暂时不使用),根本原因在于,索引0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以,常量池的索引从1而非0开始。

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

​ 常量池数量:

  1. 第9、10个字节代表着常量池数量00 18,即24-1=23。

    常量池数组:

  2. 第11个字节是0A(tag值)= 10 ,为CONSTANT_Meothdref_info,这个类型有两个index值,占4个字节00 04 00 14,即为常量池中第4个元素,和第20个元素。

    java/lang/Object.""????)V

    表示无参数列表的返回值为void的方法,即虚拟机创建的构造方法。

  3. 第16个字节为09 = 9 ,为CONSTANT_Fieldref_info,后四个字节为两个索引00 03 00 15
    即第3个元素,第21个元素。

    bytecode/MyTest1.a:I
    表示MyTest1类下的a属性的值为int类型

  4. 第21个字节为07,为CONSTANT_Class_info,后2个字节为指向全限定名常量项的索引,指向22

    bytecode/MyTest1

    表示此类的全限定名

  5. 第24字节为07,00 17指向#23。

    java/lang/Object
    表示父类的全限定名

  6. 第27字节为01,为CONSTANT_utf8_info,后2位字节为UTF-8编码的字符串长度length 00 01,表示后面有一个字节来表示这个字符,即为61 = a

    后面相同的类型,就简写了。

  7. 第31字节为01,00 01,字符串为49 = I

  8. 第35字节为01,00 06,字符串3C 69 6E 69 74 3E,表示为<init>

  9. 第43字节为01 ,00 03,28 29 56,表示为()V

  10. 第50字节为01,00 04,43 6F 64 65,表示为Code

  11. 第57字节为01,00 0F,表示为LineNumberTable

  12. 第75字节01 ,00 12,表示为LocalVariableTable

  13. 第95字节01, 00 04 ,this

  14. 第83字节01,00 12,Lbytecode/MyTest1;

  15. 第109字节01,00 04,getA

  16. 第131字节01,00 03,()I

  17. 第136字节01,00 04,setA

  18. 第143字节01, 00 04,(I)V

  19. 第151字节01,00 01,SourceFile

  20. 第163字节01,00 0C,MyTest1.java

  21. 第179字节0C,为CONSTANT_NameAndType_info,后四个字节指向方法名称#7;方法描述#8

    “”????)V

  22. 第184字节0C,为CONSTANT_NameAndType_info,后四个字节指向字段名称#5;字段描述#6

    a:I

  23. 第189字节01,00 10,bytecode/MyTest1

  24. 第207字节01,00 10,java/lang/Object

类的访问权限(2个字节)

接下来是类的访问权限access_flag,第227、228字节00 21:是0x00200x0001的并集,表示ACC_PUBLICACC_SUPER

Java字节码:深度分析Class类文件

类名(2个字节)

第229、230字节00 03为类名,是一个引用值,表示常量池中第三个常量。当前类的全限定名。

父类名(2个字节)

第231、232字节00 04为父类名,是一个引用值,表示常量池中第四个常量。当前类的父类全限定名。

接口(2+n个字节)

第233、234字节00 00为接口数,接口数为0,后面的接口名就不会出现了。

域(2+n个字节)

域的个数

第235、236字节00 01为成员变量数,为1个。

字段表集合

字段表用于描述类和接口中声明的变量。这里的字段包含了类级别的变量以及实例变量,但是不包括方法内部声明的局部变量,下图为字段表的结构。

Java字节码:深度分析Class类文件

本例中只有一个字段。

  1. 第237、238字节00 02为这个成员变量的权限访问符,表示privat
  2. 第239、240字节00 05为这个成员变量的名称描述符,指向常量池中的#5 =a
  3. 第241、242字节00 06为这个成员变量的类型描述符,指向常量池中的#6 =I
  4. 第243、244字节00 00为附加属性个数,为0,则附加属性表就不会出现。附加属性是有些编译器在编译阶段添加的值。

方法(2+n个字节)

方法个数

第245、246字节00 03为方法数,共3个方法。

方法表集合

方法表描述了一个方法的信息。

Java字节码:深度分析Class类文件

属性表集合描述了这个方法的相关属性,执行码、异常表等,JVM预定义了部分属性,但是编译器自己也可以实现自己的属性写入class文件里,供运行时使用。

不用的属性通过attribute_name_index来区分。

Java字节码:深度分析Class类文件
NO.1
  1. 第247、248字节00 01为权限访问符,表示public

  2. 第249、250字节00 07为这个方法的名称描述符,指向常量池中的#7 = <init>

  3. 第251、252字节00 08为这个方法的类型描述符,指向常量池中的#8 = ()V

  4. 第253、254字节00 01为附加属性个数。这些属性用以描述方法,由Java虚拟机根据方法的执行代码编译时计算出来的。

  5. 属性信息attribute_info:

    Code attribute的作用是保存改方法的结构,如下图。

    Java字节码:深度分析Class类文件
    • attribute_length表示attribute所包含的字节数,不包含attribute_name_indexattribute_length字段。
    • max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。
    • max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。
    • code_length表示该方法所包含的字节码的字节数以及具体的指令码。
    • 具体字节码即是该方法被调用时,虚拟机所执行的字节码。
    • exception_table,这里存放的是处理异常的信息。
    • 每个exception_table表现由start_pcend_pchander_pccatch_type组成。
    1. 第255、256字节00 09为属性值的索引,#9 为Code,表示执行代码。

    2. 第257、258、259、260字节00 00 00 38为属性长度,为56字节。

    3. 第261、262字节00 02为操作数栈的最大深度,栈深度为2。

    4. 第263、264字节00 01为局部变量个数。

    5. 第265、266、267、268字节00 00 00 0A为这个方法包含的字节数与字节码,一共占10个字节,代表这个方法真正执行的内容(也就是助记符,在字节码中只是一些16进制的符号,转换为助记符,帮助我们记忆)。

      1. 第269字节2A,对应助记符aload_0,从局部变量表中位于0slot中的变量推到操作数栈中去。
      2. 对应助记符invokespecial,调用父类的方法。
        1. 第271、272字节00 01代表这个助记符的参数 ,指向常量池#1,java/lang/Object."<init>":()V,即代表调用Object类的这个方法,即父类的构造方法。
      3. 第273字节2A,对应助记符aload_0
      4. 第274字节04 ,对应助记符iconst_1,将int类型的数1推送到操作数栈顶。
      5. 第275字节B5 ,对应助记符putfield
        1. 第276、277字节00 02代表这个助记符的参数,指向常量池#2, bytecode/MyTest1.a:I,也就是说将刚刚推到操作数栈顶的1赋予MyTest1中的a
      6. 第278字节B1,对应助记符return,返回void
    6. 第279、280字节00 00,异常表长度为0,异常表就不会出现了。

    7. 第281、282字节00 02,表示附加属性个数。

      首先是LineNumTable,行号信息,通过这个信息,可以确定,代码的所在的行数。

      Java字节码:深度分析Class类文件
      1. 第283、284字节00 0A,代表附加属性索引,指向常量池#10,LineNumberTable
      2. 第285、286、287、288字节00 00 00 0A,代表这个附加属性的长度。
      3. 第289、290字节00 02,代表有两个映射。
      4. 第291、292字节00 00,
      5. 第293、294字节00 0F,表示code数组中偏移量为0的,映射到第10行,在第10行,编译器生成了一个构造方法。
      6. 第295、296字节00 04,
      7. 第297、298字节00 11,表示code数组中偏移量为4的,映射到第17行,
        然后是LocalVariableTable,局部变量表。
      8. 第299、300字节00 0B,代表附加属性索引,指向常量池#11,LocalVariableTable
      9. 第301、302、303、304字节00 00 00 0C代表这个附加属性的长度。
      10. 第305、306字节00 01 ,代表只有一个变量。
      11. 第307、308字节00 00,代表局部变量从0开始。
      12. 第309、310字节00 0A,代表局部变量长度。

      11和12决定了局部变量的作用范围是从哪一行到哪一行。

      1. 第311字节00,代表局部变量索引。
      2. 第312字节0C,代表常量池中的#10,this,代表当前对象。
      3. 第313、314字节00 0D,表示词局部变量的描述,代表常量池中的#13, Lbytecode/MyTest1;
      4. 第315、316字节00 00,做校验检查的。

      在Java中,每个方法都可以访问this,对于非静态方法来说,至少有一个局部变量传入方法,即this。

NO.2
  1. 第317、318字节00 01为权限访问符,表示public

  2. 第319、320字节00 0E为这个方法的名称描述符,指向常量池中的#14 = getA

  3. 第321、322字节00 0F为这个方法的类型描述符,指向常量池中的#15 =()I

  4. 第323、324字节00 01为附加属性个数。这些属性用以描述方法,由Java虚拟机根据方法的执行代码编译时计算出来的。

  5. 属性信息attribute_info:

    1. 第325、326字节00 09为属性值的索引,#9 为Code,表示执行代码

    2. 第327、328、329、330字节00 00 00 2F为属性长度

    3. 第331、332字节00 01为操作数栈的最大深度

    4. 第333、334字节00 01为局部变量表最大长度

    5. 第335、336、337、338字节00 00 00 05为这个方法包含的字节数与字节码,一共占5个字节,代表这个方法真正执行的内容(助记符,在字节码中只是一些16进制的符号,转换为助记符,帮助我们记忆

      1. 第339字节2A,对应助记符aload_0,

      2. 第340字节B4,对应助记符getfiled,调用父类的方法

      3. 第341、342字节00 02代表这个助记符的参数 ,指向常量池#2, // bytecode/MyTest1.a:I
        表示从对象中获取字段

      4. 第343字节AC,对应助记符ireturn,表示返回一个int

      5. 第344、345字节00 00,异常表长度为0,异常表就不会出现了

      6. 第346、347字节00 02,表示附加属性个数:

        首先是LineNumTable,行号信息

        1. 第348、349字节00 0A,代表附加属性索引,指向常量池#10,LineNumberTable
        2. 第350、351、352、353字节00 00 00 06,代表这个附加属性的长度
        3. 第353、354字节00 01,代表有1个映射
        4. 第355、356字节00 00,
        5. 第357、358字节14 00,
        6. 表示code数组中偏移量为0的,映射到第20行,在第20行,对应return a;

        然后是LocalVariableTable,局部变量表

        1. 第359、360字节00 0B,代表附加属性索引,指向常量池#11,LocalVariableTable
        2. 第361、362、363、364字节00 00 00 0C代表这个附加属性的长度
        3. 第365、366字节00 01 ,代表只有一个变量
        4. 第367、368字节00 00,代表局部变量从0开始
        5. 第369、370字节00 05,代表局部变量长度
        6. 第371字节00,代表局部变量索引
        7. 第372字节0C,代表常量池中的#10,this,代表当前对象
        8. 第373、374字节00 0D,表示词局部变量的描述,代表常量池中的#13, Lbytecode/MyTest1;
        9. 第376、377字节00 00,做校验检查的
NO.3

第三个方法就不再叙述了,与第二个方法差不多。

从上文看出,当一个Java程序中没有构造方法时,编译器会生成一个<init>方法,即构造方法。且非静态成员变量赋值是在构造方法中完成的。若有多个构造方法,每个构造方法都会完成非静态成员变量赋值。

字节属性

  1. 第456、457字节00 01,只有一个属性。
  2. 第458、459字节00 12,指向#18 ,SourceFile
  3. 第460、461、462、463字节00 00 00 02,表示属性长度,占两个字节。
  4. 第464、465字节00 13,表示#13,MyTest1.java