Class类文件结构分析-1

引言

  我们知道,使用Java编写的类文件,在经过编译之后,变成.class文件。Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据。

  根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表,下面的解析都要以这两种数据类型为基础,这里先介绍一下。

  无符号数属于基本的数据类型,以u1、u2、u4、u8来分表代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量之或者按照UTF-8编码构成的字符串值等。

  表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合解雇的数据,整个Class文件本质上就是一张表,如下图所示:

图1-Class文件格式

分析前准备

  在分析Class文件结构之前,我们先来生成一个写一个在Java中最常见的实例类,代码如下:

public class Person {

    private String name;
    public int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

如上图所以,一个最常见的Person类,有两个属性:name和age。又生成了各自的set和get方法。下面使用javac将该Person.java类编译成Person.class。

javac Person.java

使用文本打开Person.class文件,如下所示:

cafe babe 0000 0034 001d 0a00 0500 1809
0004 0019 0900 0400 1a07 001b 0700 1c01
0004 6e61 6d65 0100 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 0003 6167
6501 0001 4901 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0007
6765 744e 616d 6501 0014 2829 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0773 6574 4e61 6d65 0100 1528 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 2956
0100 0667 6574 4167 6501 0003 2829 4901
0006 7365 7441 6765 0100 0428 4929 5601
000a 536f 7572 6365 4669 6c65 0100 0b50
6572 736f 6e2e 6a61 7661 0c00 0a00 0b0c
0006 0007 0c00 0800 0901 0016 636f 6d2f
7171 792f 6d61 7064 656d 6f2f 5065 7273
6f6e 0100 106a 6176 612f 6c61 6e67 2f4f
626a 6563 7400 2100 0400 0500 0000 0200
0200 0600 0700 0000 0100 0800 0900 0000
0500 0100 0a00 0b00 0100 0c00 0000 1d00
0100 0100 0000 052a b700 01b1 0000 0001
000d 0000 0006 0001 0000 0008 0001 000e
000f 0001 000c 0000 001d 0001 0001 0000
0005 2ab4 0002 b000 0000 0100 0d00 0000
0600 0100 0000 0e00 0100 1000 1100 0100
0c00 0000 2200 0200 0200 0000 062a 2bb5
0002 b100 0000 0100 0d00 0000 0a00 0200
0000 1200 0500 1300 0100 1200 1300 0100
0c00 0000 1d00 0100 0100 0000 052a b400
03ac 0000 0001 000d 0000 0006 0001 0000
0016 0001 0014 0015 0001 000c 0000 0022
0002 0002 0000 0006 2a1b b500 03b1 0000
0001 000d 0000 000a 0002 0000 001a 0005
001b 0001 0016 0000 0002 0017 

除此之外,我们可以通过javap命令来查看Class文件的具体信息

javap -v -c -s -l Person.class

得到的信息如下所示:

Classfile /Users/qin/AndroidStudioProjects/MapDemo/app/src/main/java/com/qqy/mapdemo/Person.class
  Last modified 2020-12-21; size 556 bytes
  MD5 checksum c03fc3c9928ccc5beba4b52b5aba890e
  Compiled from "Person.java"
public class com.qqy.mapdemo.Person
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."":()V
   #2 = Fieldref           #4.#25         // com/qqy/mapdemo/Person.name:Ljava/lang/String;
   #3 = Fieldref           #4.#26         // com/qqy/mapdemo/Person.age:I
   #4 = Class              #27            // com/qqy/mapdemo/Person
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               name
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               age
   #9 = Utf8               I
  #10 = Utf8               
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               getName
  #15 = Utf8               ()Ljava/lang/String;
  #16 = Utf8               setName
  #17 = Utf8               (Ljava/lang/String;)V
  #18 = Utf8               getAge
  #19 = Utf8               ()I
  #20 = Utf8               setAge
  #21 = Utf8               (I)V
  #22 = Utf8               SourceFile
  #23 = Utf8               Person.java
  #24 = NameAndType        #10:#11        // "":()V
  #25 = NameAndType        #6:#7          // name:Ljava/lang/String;
  #26 = NameAndType        #8:#9          // age:I
  #27 = Utf8               com/qqy/mapdemo/Person
  #28 = Utf8               java/lang/Object
{
  public int age;
    descriptor: I
    flags: ACC_PUBLIC

  public com.qqy.mapdemo.Person();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 8: 0

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 14: 0

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 18: 0
        line 19: 5

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 22: 0

  public void setAge(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #3                  // Field age:I
         5: return
      LineNumberTable:
        line 26: 0
        line 27: 5
}
SourceFile: "Person.java"

Class文件结构

  在以上准备工作做好之后,我们来正式开始分析Person.class中十六进制的数据是怎么排列的,以及各个部分都是如何表示类信息的。

魔数

每一个字节码文件的开头都是一个确定的占四个字节的16进制的数字,我们把这个叫做魔数,值为:cafe babe。这个值是确定的,每一个.class文件的开头都必须是该值。

Java版本

魔数之后是Java的版本信息,也是占四个字节,其中前两个字节是次版本,后两个字节是主版本。这里的值是:0000 0034,转换为10进制,对应的Java版本中,次版本是0,主版本是52(1.8),所以Java版本是1.8.0,我们看下本机的Java版本:

java version "1.8.0_131"
常量池

紧接着是常量池,常量值的所占长度是不确定的,其中前两个字节是常量池的长度,001d,对应的十进制是 16 + 13 - 1 = 28,为什么要减一呢?因为0是被JVM虚拟机占用的,我们看下被我们通过javap命令得出的字节码文件结构中的常量池部分:

Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."":()V
   #2 = Fieldref           #4.#25         // com/qqy/mapdemo/Person.name:Ljava/lang/String;
   #3 = Fieldref           #4.#26         // com/qqy/mapdemo/Person.age:I
   #4 = Class              #27            // com/qqy/mapdemo/Person
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               name
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               age
   #9 = Utf8               I
  #10 = Utf8               
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               getName
  #15 = Utf8               ()Ljava/lang/String;
  #16 = Utf8               setName
  #17 = Utf8               (Ljava/lang/String;)V
  #18 = Utf8               getAge
  #19 = Utf8               ()I
  #20 = Utf8               setAge
  #21 = Utf8               (I)V
  #22 = Utf8               SourceFile
  #23 = Utf8               Person.java
  #24 = NameAndType        #10:#11        // "":()V
  #25 = NameAndType        #6:#7          // name:Ljava/lang/String;
  #26 = NameAndType        #8:#9          // age:I
  #27 = Utf8               com/qqy/mapdemo/Person
  #28 = Utf8               java/lang/Object

这部分非常重要,因为在下面的方法、类信息、属性等等的分析 中,都会用到这部分的内容,下面我们花点时间,把上述的28个常量挨个分析一遍。

分析这部分的内容,我们需要用到一个前面提到的表结构说明,如下图所示:

图2-常量表结构.jpg

常量1

tag值为0a,转换为10进制是10,因此类型是:CONSTANT_Methodref_info。接着是两个占两个字节的index,0005 0018,分别对应05#和24#,我们看下图3:

#1 = Methodref          #5.#24         // java/lang/Object."":()V

至于后面的信息,我们稍后再介绍。可以看到,通过javap命令得到的信息验证了我们分信息得出的结论。

常量2

tag值为09,类型为:CONSTANT_Fieldref_info,接着两个index,0004 0019,分别对应04#和25#,表示字段name。

#2 = Fieldref           #4.#25         // com/qqy/mapdemo/Person.name:Ljava/lang/String;
常量3

tag值为09,类型为:CONSTANT_Fieldref_info,接着两个index,0004 001a ,分别对应#04和#26,表示字段age。

#3 = Fieldref           #4.#26         // com/qqy/mapdemo/Person.age:I
常量4

tag值为07,类型为:CONSTANT_Class_info,接着是两个字节的index,值为:001b,对应索引值为:#27,表示类信息

 #4 = Class              #27            // com/qqy/mapdemo/Person
常量5

tag值为07,类型为:CONSTANT_Class_info,接着是两个字节的index,值为:001c,对应索引值为#28,表示类信息

 #5 = Class              #28            // java/lang/Object

####### 常量6
tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 04。这里的长度是4,我们往后数四个字节的数据,6e 61 6d 65,对应ASCII表,值为:name,对应字段名称。

 #6 = Utf8               name
常量7

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 12。这里的长度是18,我们往后数18个字节的数据,4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b,对应ASCII表,值为:Ljava/lang/String;

#7 = Utf8               Ljava/lang/String;
常量8

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 03。这里的长度是3,我们往后数3个字节的数据,61 67 65,对应ASCII表,值为:age,对应字段名称。

 #8 = Utf8               age
常量9

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 01。这里的长度是1,我们往后数1个字节的数据,49,对应ASCII表,值为:I,标识Integer。

  #9 = Utf8               I
常量10

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 06。这里的长度6,我们往后数6个字节的数据,3c 69 6e 69 74 3e ,对应ASCII表,值为:,表明是构造方法。

#10 = Utf8               
常量11

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 03。这里的长度6,我们往后数3个字节的数据:28 29 56 ,对应ASCII表,值为:()V,表明构造方法的没有入参,返回值是Void。这是JVM默认给加的默认的构造方法。

常量12

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 04。这里的长度4,我们往后数4个字节的数据:43 6f 64 65 ,对应ASCII表,值为:Code。这里的Code很重要,在Code里面存放的是JVM指令集。

#12 = Utf8               Code
常量13

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 0f。这里的长度15,我们往后数15个字节的数据:4c 69
6e 65 4e 75 6d 62 65 72 54 61 62 6c 65,对应ASCII表,值为:LineNumberTable。代表的意思是行号表,表示JVM指令和Java代码的映射关系,JVM在执行指令的时候,如果有异常可以精准定位到Java代码中,是因为这个。

#13 = Utf8               LineNumberTable
常量14

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 07。这里的长度7,我们往后数7个字节的数据:67 65 74 4e 61 6d 65,对应ASCII表,值为:getName,表示方法名。

 #14 = Utf8               getName
常量15

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 14。这里的长度20,我们往后数20个字节的数据:28 29 4c 6a 61 76
61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b ,对应ASCII表,值为:()Ljava/lang/String;,表示方法没有入参,返回值是String类型。

 #15 = Utf8               ()Ljava/lang/String;
常量16

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 07。这里的长度7,我们往后数7个字节的数据:07 73 65 74 4e 61 6d 65 ,对应ASCII表,值为:setName,表示setName方法。

 #16 = Utf8               setName
常量17

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 15。这里的长度21,我们往后数21个字节的数据:28 4c 6a 61 76
61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 ,对应ASCII表,值为:(Ljava/lang/String;)V。这里表示方法的入参是String类型,返回值是Void类型。

#17 = Utf8               (Ljava/lang/String;)V
常量18

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 06。这里的长度6,我们往后数6个字节的数据:67 65 74 41 67 65 ,对应ASCII表,值为:getAge,表示getAge方法。

 #18 = Utf8               getAge
常量19

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 03。这里的长度3,我们往后数3个字节的数据:28 29 49 ,对应ASCII表,值为:()I,表示方法没有入参,返回值是Integer类型。

 #19 = Utf8               ()I
常量20

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 06。这里的长度6,我们往后数6个字节的数据:73 65 74 41 67 65 ,对应ASCII表,值为:setAge,表示setAge方法。

#20 = Utf8               setAge
常量21

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 04。这里的长度4,我们往后数4个字节的数据:28 49 29 56,对应ASCII表,值为:(I)V,表示方法的入参是Integer类型,返回值是Void。

  #21 = Utf8               (I)V
常量22

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 0a。这里的长度10,我们往后数10个字节的数据:53 6f 75 72 63 65 46 69 6c 65 ,对应ASCII表,值为:SourceFile。表示源文件。

  #22 = Utf8               SourceFile
常量23

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 0b。这里的长度11,我们往后数11个字节的数据:50 65 72 73 6f 6e 2e 6a 61 76 61 ,对应ASCII表,值为:Person.java。表示源文件是Persion.java。

  #23 = Utf8               Person.java
常量24

tag为0c,类型为:CONSTANT_NameAndType_info,接着是长度,占四个字节,值为:00 0a 00 0b 。表示是#10和#11。

#24 = NameAndType        #10:#11        // "":()V
常量25

tag为0c,类型为:CONSTANT_NameAndType_info,接着是长度,占四个字节,值为:00 06 00 07。表示#06和#07。

 #25 = NameAndType        #6:#7          // name:Ljava/lang/String;
常量26

tag为0c,类型为:CONSTANT_NameAndType_info,接着是长度,占四个字节,值为:00 08 00 09。表示#08和#09。

#26 = NameAndType        #8:#9          // age:I
常量27

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 16。这里的长度22,我们往后数22个字节的数据:63 6f 6d 2f
71 71 79 2f 6d 61 70 64 65 6d 6f 2f 50 65 72 73
6f 6e ,对应ASCII表,值为:com/qqy/mapdemo/Person。表示类所在的Package。

  #27 = Utf8               com/qqy/mapdemo/Person
常量28

tag值为01,类型为:CONSTANT_Utf8_info,接着是长度,占两个字节:00 10。这里的长度16,我们往后数16个字节的数据:6a 61 76 61 2f 6c 61 6e 67 2f 4f
62 6a 65 63 74 ,对应ASCII表,值为:java/lang/Object。

  #28 = Utf8               java/lang/Object

到这里,常量池的部分已经分析完了。在分析每一个常量的时候,各自代表的意思已经说得很清楚了,这里就不再一一赘述了。

Access Flags

通过字节码结构图我们知道,常量池下面是Access Flags,即访问标志,占两个字节,值为:00 21。看下面一张图:

图3-类访问标志

我们可以看到,图里面并没有Ox0021,这是因为该值是0x0020和0x0001的并集,即ACC_PUBLIC和ACC_SUPER。这里标识该类是public的,并且可以访问父类。

This Class Name

访问标识之后是类名,占两个字节,值为:00 04。这里表示指向常量池中#04的信息,我们看上面的常量池的#4值,为:com/qqy/mapdemo/Person。

Super Class Name

This Class Name后面是Super Class Name,即父类的名字。占两个字节,值为:00 05。这里标识指向常量池中#05的信息,我们看上面的常量池的#05的值

#5 = Class              #28            // java/lang/Object

值为:java/lang/Object。

Interfaces

接口,这里占的2 + n个字节,包括两部分,第一部分是interfaces_count(接口的个数),第二部分是interfaces(接口名)。我们来看第一部分的两个字节,值为:00 00。表示接口个数为0,因此该类没有实现任何接口,所以下面的interfaces就不存在了。如果count大于0的话,那接口表是存在的。有兴趣的同学可以自行去实现一下看卡。
我们知道,在Java中,一个类最多实现的接口个数是65535,那么这个65535是怎么来的呢?答案就在这里,接口个数占2个字节,也就是16位,如果16位全部是1,那就是65535,所以最大值是65535。

Fields

字段表。跟上面的接口一样,也是占2 + n个字节,包括两部分。其中第一部分是字段表的长度,占两个字节,值为:00 02,表示有两个字段。这里就是我们的name字段和age字段了。

后面接跟着是filed_info[],是一个描述字段的数组。filed_info的结构是什么样呢?如下图所示:

图4-字段表结构

再来看下字段访问标志,如下图:

图5-字段访问标志

如图4所示,每一个字段需要访问标志、字段名称和字段类型来唯一确定。 attribute_count标识属性表长度,attribute_info表示字段属性。
我们接着分析,字段的访问标志占两个字节,我们接着数两个字节:0002。通过图5我们可以得到0x0002代表的是ACC_PRIVATE,是私有的。

接着往下,下面是字段名称索引,占两个字节,值为:00 06,都应常量池中的#06,我们去上面常量池中可以知道,#06对应的是name,即字段名称是name。

再接着往下,是字段索引描述,同样占两个字符,值为:00 07,对应的是#07,查常量值知道,对应的是:Ljava/lang/String,即字段是String类型的。

接着再往下,属性表个数,占两个字节,值为:00 00。因此没有属性表。该字段到此就结束了。总结起来就是,私有的、类型为String类型的、名称为name的字段,即我们java代码中的:

private String name;

接着往下,下一个字段的访问标志位占两个字节, 00 01,查表为:ACC_PUBLIC,表示公有。

接着两个字节为字段名称索引,占两个字节,值为:00 08,对应的是#08,查常量值知道,对应的是:age。表示字段名称是age。

再往下两个字节为字段的索引描述,同样占两个字符,值为:00 09,对应的是#9,查常量值知道,对应的是:I,即为int类型。

再往下两个字节,表示字段属性长度,值为:00 00。所以这里没有属性。该字段到此结束,综上为:

private int age;
Methods

方法,占用2 + n个字节,其中前两个字节表示方法的个数,值为:00 05。说明有五个方法,我们来数一下:setName/getName/setAge/getAge/构造方法,正好五个。接下来是方法表,方法表的结构比较复杂,我们来一一看下。
老规矩,先来看下方法表结构:


图6-方法表结构

接着再来看下,方法的访问标志:


图7-方法访问标志
方法-1

前两个字节为方法访问权限,占两个字节,值为:00 01,查表可知,为:ACC_PUBLIC,表示公有的。

接下来的两个字节表示方法名称的索引,值为00 0a,十进制为10,看下常量池中#10对应的值为 ,表示构造方法。

接着看,还是两个字节,表示方法描述索引,值为:00 0b,十进制为11,看下常量池中#11对应的值为:()V,说明构造方法是无入参,返回值Void。

再往后的两个字节表示方法的属性个数,值为:00 01,表示有一个属性。

接下来看属性信息,先看下属性表结构:

图8-属性表结构

两个字节,表示属性的名称指向常量池的索引,值为:00 0c,十进制为12,看下常量池对应的#12的数据为Code。这个Code很重要,表示字节码指令的信息,说明此属性是方法的字节码描述。

接下来的四个字节,为Code内容的长度,值为:00 00 00 1d,十进制为29,表示Code长度为29。详情为:

00 01 00 01 00 00 00 05 2a b7 00 01 b1 
00 00 00 01 00 0d 00 00 00 06 00 01 00 00 00 08 

我们看下Code对应的属性结构:


图9-Code属性的表结构

上面我们已经分析了attribute_name_index和attribute_length,因此Code长度的29个字节是从max_stack开始的。
max_stack,方法最大操作数栈的深度,占两个字节,值为:00 01,十进制为1,表示最大操作数栈的深度为1。

max_locals,方法的局部变量表的个数,占两个字节,值为00 01,十进制为1,表示局部变量表的个数为1。

Code_length,指令码的长度,表示有该方法对应多少个指令。占四个字节,值为:00 00 00 05,十进制为5,说明有五个指令码。

Code[Code_length],具体指令码,因为有五个指令码,所以长度为5个字节,值为:2a b7 00 01 b1。

编号 具体指令(助记符)
0 aload_0
1 invokespecial #1
4 return

这里需要说明一下,机器能够认识的是指令码,也就是我们拿到的十六进制的数据,至于上述的aload_0或者invokespecial等等,我们称之为助记符。

先来来看下,这个指令码对应的分别是什么。

这里给大家推荐一个插件,名字叫Jclasslib,通过这个插件我们可以看到具体的指令信息。如图所示:


图10-构造方法

在红框中的信息我们可以点击,会自动打开浏览器,我们看下具体信息:


图11-aload_n

可以看到,aload_0对应的十六进制是0x2a。接着往下看,点击invokespecial:


图12-invokespecial

可以看到,invokespecial对应的十六进制是0xb7。invokespecial的意思是调用父类的构造方法,那么具体是哪个父类的?这就是b7后面后面跟着的00 01决定的,00 01对应的常量池索引是#1,我们看下,在常量池中#1对应的是java/lang/Object :()V,这里的意思就是Object类的构造方法。
我们再点开return看一下:


图12-return

可以看到,return对应的值是0xb1。

虚拟机在读到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译"2A B7 00 0A B1"的过程为:
1.读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
2.读入B7,查表得B7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法。
3.读入00 0A,这是invokespecial的参数,查常量池可以为实力构造器方法的符号引用。
4.读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为Void。这条指令执行后,当前方法结束。

接下来是异常表长度,占两个字节,值为:00 00,长度是0,表示该方法不会抛出异常。

接着往下,两个字节00 01表示属性表长度为1。
两个字节是常量池索引,00 0d,十进制为13,我们看下具体值为:LineNumberTable,表示字节码指令和java代码行数的映射。下图表示LineNumberTable的属性结构:

图13-LineNumberTable属性结构

在图中的line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。因此我们可以得出,在00 0d之后的两个字节表示的是line_number_table_length,值为:00 01,表示line_number_table的长度为1,即00 00 代表字节码行号, 00 08代表Java源码行号。
到这里,构造方法已经分析结束了。这里要注意一点的是,javap输出的arg_size的值可能会有疑问,构造方法是没有入参的啊,这里的长度怎么会是1呢?而且不管是在参数列表里面还是方法体内,都没有定义任何局部变量,那locals怎么也是1呢?这是因为,在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用。

方法-2

第二个方法为getName,我们看下getName的javap信息:


图14-getName方法

前两个字节00 01表示方法访问标志,查表可知为ACC_PUBLIC,对应为public。

接下来,00 0e,十进制为14,代表常量池索引#14,值为:getName。

00 0f,表示方法描述的索引,十进制为15,代表常量池#15,值为:()Ljava/lang/String;,表示方法没有入参,返回值是String。

接下来是:00 01,代表方法的属性个数,值为1。说明只有一个属性。

接下来的两个字节,00 0c,表示属性的名称指向常量池的索引,十进制为12,看一下常量池#12,值为:Code。表示的是Code属性。

接下来的四个字节表示Code内容的长度,值为:00 00 00 1d,十进制为:29。

这里把数据直接列到这里,方便下面查看:00 01 00 01 00 00 00 05 2a b4 00 02 b0 00 00 00 01 00 0d 00 00 00 06 00 01 00 00 00 0e

00 01 表示方法的最大操作数栈的深度,值为1

00 01 表示局部变量表的个数,值为1

00 00 00 05 表示指令码的长度,为5

2a b4 00 02 b0为指令码,其中2a表示aload_0,b4表示getField,其中00 02表示常量池中#2,值为:name。areturn表示返回常量池中的#2对应的值,即返回name。

00 00表示异常信息的大小是0,因此没有异常信息。
00 01表示属性表的大小是1

00 0d表示是常量池中的#13,值为:LineNumberTable。

接着的四个字节是属性的长度,00 00 00 06,表示长度为6。

接着的两个字节表示行号表的长度,00 01,长度为1。

接下来的四个字节,前两个字节代表字节码行号,后两个自己代表Java源码行号。00 00表示aload_0,00 0e表示Java类中的第14行

 return name;
方法-3

第三个方法是setName,我们看下setName的javap信息:


图15-setName_javap

前两个字节00 01表示方法访问标志,查表可知为ACC_PUBLIC,对应为public。

接下来的两个字节00 10表示方法名字,对应常量池中的#16,为:setName。

再往下两个字节00 11表示方法描述索引,对应常量池中#17,为: (Ljava/lang/String;)V,表示方法的入参是String类型,返回值为Void。

再往下两个字节00 01表示属性表的长度,值为1。

接下来的两个字节00 0c表示属性值的索引,对应12,值为:Code。说明这唯一的一个属性是Code属性,接下来转到Code属性表中。

再往下四个字节表示Code属性表的长度,值为:00 00 00 22,转为十进制为:2 * 16 + 2 = 34,往后数34个字节,该方法结束。值为:

00 02 00 02 00 00 00 06 2a 2b b5 00 02 b1 00 00 00
01 00 0d 00 00 00 0a 00 02 00 00 00 12 00 05 00 13

00 02为max_stack,表示方法的最大操作数栈的深度,值为2

00 02位max_locals,表示局部变量表的个数,值为2

00 00 00 06表示字节码指令的个数,长度为6,为:2a 2b b5 00 02 b1。对应:

编号 具体指令(助记符)
0 aload_0
1 aload_1
2 putfield #2
5 return

接下来的00 00,表示异常表的个数是0,跳过。

00 01表示属性表的大小为1,再往下取两个字节00 0d,表示具体的属性名称,对应常量池中的#13,为:LineNumberTable。

接下来转到LineNumberTable属性结构中。
00 00 00 0a 00 02 00 00 00 12 00 05 00 13

00 00 00 0a表示属性长度,值为:10,正好对应后面的10个字节。

00 02表示line_number_table_length,属性个数为2,对应关系为

字节码 Java源码行数
00 00-0 00 12-18
00 05-5 00 13-19
方法-4

第四个方法是getAge,我们看下getAge的javap信息:


图16-getAge_javap

前两个字节00 01表示方法访问标志,查表可知为ACC_PUBLIC,对应为public。

接下来的两个字节00 12表示方法名字,对应常量池中的#18,为:getAge。

再往下两个字节00 13表示方法描述索引,对应常量池中#19,为: ()I
,表示方法没有入参,返回值为int类型。

再往下两个字节00 01表示属性表的长度,值为1。

接下来的两个字节00 0c表示属性值的索引,对应12,值为:Code。说明这唯一的一个属性是Code属性,接下来转到Code属性表中。

再往下四个字节表示Code属性表的长度,值为:00 00 00 1d,转为十进制为:1 * 16 + 13 = 29,往后数29个字节,该方法结束。值为:

00 01 00 01 00 00 00 05 2a b4 00 03 ac 
00 00 00 01 00 0d 00 00 00 06 00 01 00 00 00 16 

00 01为max_stack,表示方法的最大操作数栈的深度,值为1

00 01位max_locals,表示局部变量表的个数,值为1

00 00 00 05表示字节码指令的个数,长度为5,为:2a b4 00 03 ac 。对应:

编号 具体指令(助记符)
0 aload_0
1 getfield #3
4 ireturn

接下来的00 00,表示异常表的个数是0,跳过。

00 01表示属性表的大小为1,再往下取两个字节00 0d,表示具体的属性名称,对应常量池中的#13,为:LineNumberTable。

接下来转到LineNumberTable属性结构中。
00 00 00 06 00 01 00 00 00 16

00 00 00 06表示属性长度,值为:6,正好对应后面的6个字节。

00 01表示line_number_table_length,属性个数为1,对应关系为

字节码 Java源码行数
00 00-0 00 16-22
方法-5

第三个方法是setAge,我们看下setAge的javap信息:


图17-setAge_javap

前两个字节00 01表示方法访问标志,查表可知为ACC_PUBLIC,对应为public。

接下来的两个字节00 14表示方法名字,对应常量池中的#20,为:setAge。

再往下两个字节00 15表示方法描述索引,对应常量池中#21,为: (I)V,表示方法的入参是int类型,返回值为Void。

再往下两个字节00 01表示属性表的长度,值为1。

接下来的两个字节00 0c表示属性值的索引,对应12,值为:Code。说明这唯一的一个属性是Code属性,接下来转到Code属性表中。

再往下四个字节表示Code属性表的长度,值为:00 00 00 22,转为十进制为:2 * 16 + 2 = 34,往后数34个字节,该方法结束。值为:

00 02 00 02 00 00 00 06 2a 1b b5 00 03 b1 00 00 00 
01 00 0d 00 00 00 0a 00 02 00 00 00 1a 00 05 00 1b 

00 02为max_stack,表示方法的最大操作数栈的深度,值为2

00 02位max_locals,表示局部变量表的个数,值为2

00 00 00 06表示字节码指令的个数,长度为6,为:2a 1b b5 00 03 b1。对应:

编号 具体指令(助记符)
0 aload_0
1 iload_1
2 putfield #3
5 return

接下来的00 00,表示异常表的个数是0,跳过。

00 01表示属性表的大小为1,再往下取两个字节00 0d,表示具体的属性名称,对应常量池中的#13,为:LineNumberTable。

接下来转到LineNumberTable属性结构中。
00 00 00 0a 00 02 00 00 00 1a 00 05 00 1b

00 00 00 0a表示属性长度,值为:10,正好对应后面的10个字节。

00 02表示line_number_table_length,属性个数为2,对应关系为

字节码 Java源码行数
00 00-0 00 1a-26
00 05-5 00 1b-27

到这里,五个方法已经全部分析完成了。重新回到Class类结构表中,可以看到接下来的两个字节表示attributes_count,值为:00 01,表示还有一个属性。
再往下看两个字节,00 16,对应的是常量池中的#22,值为:SourceFile。回到属性表结构图8中查看,接下来四个字节表示属性的长度,00 00 00 02表示长度为2个字节,值为:00 17,对应从常量表中的#23,值为:Person.java。

结语

  到这里,这篇文章就要结束了。作为字节码分析的第一篇文章,本文给大家介绍了整体Class文件结构,以及结构表中的每一部分代表了什么,并通过一个简单的Person类带大家进行了一次整体的分析。当然了,字节码文件中还有许多是我们没有讲到的,比如:我们在Person类中的方法中,异常信息都是 00 00,那如果有异常的情况是什么呢?再比如,我们遇到的属性都是Code属性,那么其他的属性呢?等等......这些问题,会在后续的文章中逐一为大家讲解。
bye~

参考资料

深入理解Java虚拟机-JVM高级特性与最佳实践(第2版)

你可能感兴趣的:(Class类文件结构分析-1)