Java字节码解读

Java号称“一次编译到处运行”,但是我们对这句话理解深度有多少呢?我们写的Java文件通过编译器编译成class文件,Java虚拟机执行的就是class文件。不论class文件来自哪里,由哪种编译器编译,或者是手写的class文件,只要符合Java虚拟机规范,那么它就能被虚拟机执行。

在读字节码的时候千万不能急,这东西只能慢慢来,稍不留神就可能会一脸懵。所以大家保持平常心,一次没看懂也没关系,至少对字节码有了一定的了解。

示例代码

package com.doaredo.test;

public class Bytecode implements Serializable {

    private int value;

    public synchronized void add(){
        value++;
    }

    public void incr(){
        synchronized (this){
            value++;
        }
    }

    public static void main(String[] args) {
        Bytecode bytecode = new Bytecode();
        bytecode.add();
        bytecode.incr();
        System.out.println(bytecode.value);
    }
}

查看字节码

用文本编辑工具(如Editplus)以Hex方式打开示例代码的class文件,Mac上直接用Sublime等打开就可以看到,如下图所示,文件中显示为16进制代码,开头的4个字节称为魔数,唯有以 cafe babe开头的class文件才能被虚拟机接受:

cafe babe 0000 0034 0033 0a00 0900 2109
0003 0022 0700 230a 0003 0021 0a00 0300
240a 0003 0025 0900 2600 270a 0028 0029
0700 2a07 002b 0100 0576 616c 7565 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 1b4c 636f 6d2f 646f
6172 6564 6f2f 7465 7374 2f42 7974 6563
6f64 653b 0100 0361 6464 0100 0469 6e63
7201 000d 5374 6163 6b4d 6170 5461 626c
6507 0023 0700 2a07 002c 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0461 7267
7301 0013 5b4c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b01 0008 6279 7465 636f
6465 0100 0a53 6f75 7263 6546 696c 6501
000d 4279 7465 636f 6465 2e6a 6176 610c
000d 000e 0c00 0b00 0c01 0019 636f 6d2f
646f 6172 6564 6f2f 7465 7374 2f42 7974
6563 6f64 650c 0014 000e 0c00 1500 0e07
002d 0c00 2e00 2f07 0030 0c00 3100 3201
0010 6a61 7661 2f6c 616e 672f 4f62 6a65
6374 0100 146a 6176 612f 696f 2f53 6572
6961 6c69 7a61 626c 6501 0013 6a61 7661
2f6c 616e 672f 5468 726f 7761 626c 6501
0010 6a61 7661 2f6c 616e 672f 5379 7374
656d 0100 036f 7574 0100 154c 6a61 7661
2f69 6f2f 5072 696e 7453 7472 6561 6d3b
0100 136a 6176 612f 696f 2f50 7269 6e74
5374 7265 616d 0100 0770 7269 6e74 6c6e
0100 0428 4929 5600 2100 0300 0900 0100
0a00 0100 0200 0b00 0c00 0000 0400 0100
0d00 0e00 0100 0f00 0000 2f00 0100 0100
0000 052a b700 01b1 0000 0002 0010 0000
0006 0001 0000 0005 0011 0000 000c 0001
0000 0005 0012 0013 0000 0021 0014 000e
0001 000f 0000 0039 0003 0001 0000 000b
2a59 b400 0204 60b5 0002 b100 0000 0200
1000 0000 0a00 0200 0000 0a00 0a00 0b00
1100 0000 0c00 0100 0000 0b00 1200 1300
0000 0100 1500 0e00 0100 0f00 0000 7a00
0300 0300 0000 192a 594c c22a 59b4 0002
0460 b500 022b c3a7 0008 4d2b c32c bfb1
0002 0004 0010 0013 0000 0013 0016 0013
0000 0003 0010 0000 0012 0004 0000 000e
0004 000f 000e 0010 0018 0011 0011 0000
000c 0001 0000 0019 0012 0013 0000 0016
0000 0015 0002 ff00 1300 0207 0017 0700
1800 0107 0019 fa00 0400 0900 1a00 1b00
0100 0f00 0000 5f00 0200 0200 0000 1bbb
0003 59b7 0004 4c2b b600 052b b600 06b2
0007 2bb4 0002 b600 08b1 0000 0002 0010
0000 0016 0005 0000 0014 0008 0015 000c
0016 0010 0017 001a 0018 0011 0000 0016
0002 0000 001b 001c 001d 0000 0008 0013
001e 0013 0001 0001 001f 0000 0002 0020

这样的16进制的文件非常人所能看懂,我们可以反编译字节码文件,以指令形式来查看,更容易看懂的方式。使用内置的一个反编译工具javap可以反编译字节码文件,在class文件目录输入如下命令:

localhost% javap -verbose -p Bytecode.class
Classfile /Users/Neo/gitee/doaredo-markdown/doaredo/demo/idea-demo/target/classes/com/doaredo/test/Bytecode.class
  Last modified Mar 4, 2021; size 928 bytes
  MD5 checksum 4ee4fc443e5df3974b46ba2c976c537e
  Compiled from "Bytecode.java"
public class com.doaredo.test.Bytecode implements java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#33         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#34         // com/doaredo/test/Bytecode.value:I
   #3 = Class              #35            // com/doaredo/test/Bytecode
   #4 = Methodref          #3.#33         // com/doaredo/test/Bytecode."<init>":()V
   #5 = Methodref          #3.#36         // com/doaredo/test/Bytecode.add:()V
   #6 = Methodref          #3.#37         // com/doaredo/test/Bytecode.incr:()V
   #7 = Fieldref           #38.#39        // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Methodref          #40.#41        // java/io/PrintStream.println:(I)V
   #9 = Class              #42            // java/lang/Object
  #10 = Class              #43            // java/io/Serializable
  #11 = Utf8               value
  #12 = Utf8               I
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcom/doaredo/test/Bytecode;
  #20 = Utf8               add
  #21 = Utf8               incr
  #22 = Utf8               StackMapTable
  #23 = Class              #35            // com/doaredo/test/Bytecode
  #24 = Class              #42            // java/lang/Object
  #25 = Class              #44            // java/lang/Throwable
  #26 = Utf8               main
  #27 = Utf8               ([Ljava/lang/String;)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               bytecode
  #31 = Utf8               SourceFile
  #32 = Utf8               Bytecode.java
  #33 = NameAndType        #13:#14        // "<init>":()V
  #34 = NameAndType        #11:#12        // value:I
  #35 = Utf8               com/doaredo/test/Bytecode
  #36 = NameAndType        #20:#14        // add:()V
  #37 = NameAndType        #21:#14        // incr:()V
  #38 = Class              #45            // java/lang/System
  #39 = NameAndType        #46:#47        // out:Ljava/io/PrintStream;
  #40 = Class              #48            // java/io/PrintStream
  #41 = NameAndType        #49:#50        // println:(I)V
  #42 = Utf8               java/lang/Object
  #43 = Utf8               java/io/Serializable
  #44 = Utf8               java/lang/Throwable
  #45 = Utf8               java/lang/System
  #46 = Utf8               out
  #47 = Utf8               Ljava/io/PrintStream;
  #48 = Utf8               java/io/PrintStream
  #49 = Utf8               println
  #50 = Utf8               (I)V
{
  private int value;
    descriptor: I
    flags: ACC_PRIVATE

  public com.doaredo.test.Bytecode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/doaredo/test/Bytecode;

  public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field value:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field value:I
        10: return
      LineNumberTable:
        line 10: 0
        line 11: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/doaredo/test/Bytecode;

  public void incr();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field value:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field value:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 14: 0
        line 15: 4
        line 16: 14
        line 17: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  this   Lcom/doaredo/test/Bytecode;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class com/doaredo/test/Bytecode, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #3                  // class com/doaredo/test/Bytecode
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #5                  // Method add:()V
        12: aload_1
        13: invokevirtual #6                  // Method incr:()V
        16: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        19: aload_1
        20: getfield      #2                  // Field value:I
        23: invokevirtual #8                  // Method java/io/PrintStream.println:(I)V
        26: return
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 12
        line 23: 16
        line 24: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  args   [Ljava/lang/String;
            8      19     1 bytecode   Lcom/doaredo/test/Bytecode;
}
SourceFile: "Bytecode.java"

字节码结构

字节码结构如下:

类型长度数量名称说明
u44个字节1Magic Number魔数,识别Class文件
u22个字节1minor_version副版本号
u22个字节1major_version主版本号
u22个字节1constant_pool_count常量池计数器
cp_infon个字节constant_pool_count-1constant_pool常量池
u22个字节1Access Flags访问标志
u22个字节1This Class Name类索引
u22个字节1Super Class Name父类索引
u22个字节1interfaces_count接口计数器
u2n个字节interfaces_countInterfaces接口索引集合
u22个字节1fields_count字段个数
field_infon个字节fields_countFields字段集合
u22个字节1methods_count方法计数器
method_infon个字节methods_countMethods方法集合
u22个字节1attributes_count附加属性计数器
attribute_infon个字节attributes_countAttributes附加属性集合

我们看第一列的类型,在class文件中只有两种数据类型:无符号数和表:

类型定义说明
无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值无符号数属于基本的数据类型。以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节
由多个无符号数或其他表构成的复合数据结构所有的表都以"_info"结尾。表没有固定长度,通常会在其前面加上个数说明

魔数

Java中的魔数是固定的CAFEBABE,也就是上面16进制文件中的前8个字母。

魔数其实就是用来区分文件类型的一种标志,只有以CAFEBABE开头的class文件才会被Java虚拟机加载。

CAFEBABE大家通常都说成咖啡宝贝,Java的图标大概也跟这有关系吧。。。

版本号

魔数后面的4个字节就是用来表示版本号的,版本号分为主、副版本号,上面16进制文件中的版本号为:

00 00 00 34

其中前2个字节表示副版本号,后2个字节表示主版本号。16进制0034转换成10进制就是52,这里的52就对应JDK1.8,所以版本号就是1.8.0。我们有时候运行代码就会抛出52这个玩意,说Jdk版本不对。

常量池

cafe babe 0000 0034 0033 0a00 0900 2109
0003 0022 0700 230a 0003 0021 0a00 0300
240a 0003 0025 0900 2600 270a 0028 0029

在16进制文件中,版本号后面,前两个字节表示常量个数(也就是上面的0033,转换成10进制,也就是51个个常量,其中第一个常量做为备用,所以真正常量的个数为50个)。在常量池计数器的后一个字节,也就是0033后面的0a,0a转换成10进制的结果为10,我们需要对照下图表格,找到tag为10的常量。

image-20210127235357903

  • 第一个常量表:0a转换成10进制就是10,对应下图中的CONSTANT_Methodref_info,对应两个索引,各占两个字节(2bit):

    0009 (0009转换成10进制为9,表示对应位置在第9个常量表位置)

    0021 (0021转换成10进制为33,表示对应位置在第33个常量表位置)

  • 第二个常量表:09转换成10进制就是9,对应下图中的CONSTANT_Fieldref_info,对应了两个索引,各占两个字节(2bit):

    0003 (0003转换成10进制为3,表示对应位置在第3个常量表位置)

    0022 (0022转换成10进制为34,表示对应位置在第34个常量表位置)

我们一步步对照上面表格分析了16进制文件中的常量池,我们来看一下javap生成的字节码文件中对应的常量池:

Constant pool:
   #1 = Methodref          #9.#33         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#34         // com/doaredo/test/Bytecode.value:I
   #3 = Class              #35            // com/doaredo/test/Bytecode
   #4 = Methodref          #3.#33         // com/doaredo/test/Bytecode."<init>":()V
   #5 = Methodref          #3.#36         // com/doaredo/test/Bytecode.add:()V
   #6 = Methodref          #3.#37         // com/doaredo/test/Bytecode.incr:()V
   #7 = Fieldref           #38.#39        // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Methodref          #40.#41        // java/io/PrintStream.println:(I)V
   #9 = Class              #42            // java/lang/Object
  #10 = Class              #43            // java/io/Serializable
  #11 = Utf8               value
  #12 = Utf8               I
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcom/doaredo/test/Bytecode;
  #20 = Utf8               add
  #21 = Utf8               incr
  #22 = Utf8               StackMapTable
  #23 = Class              #35            // com/doaredo/test/Bytecode
  #24 = Class              #42            // java/lang/Object
  #25 = Class              #44            // java/lang/Throwable
  #26 = Utf8               main
  #27 = Utf8               ([Ljava/lang/String;)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               bytecode
  #31 = Utf8               SourceFile
  #32 = Utf8               Bytecode.java
  #33 = NameAndType        #13:#14        // "<init>":()V
  #34 = NameAndType        #11:#12        // value:I
  #35 = Utf8               com/doaredo/test/Bytecode
  #36 = NameAndType        #20:#14        // add:()V
  #37 = NameAndType        #21:#14        // incr:()V
  #38 = Class              #45            // java/lang/System
  #39 = NameAndType        #46:#47        // out:Ljava/io/PrintStream;
  #40 = Class              #48            // java/io/PrintStream
  #41 = NameAndType        #49:#50        // println:(I)V
  #42 = Utf8               java/lang/Object
  #43 = Utf8               java/io/Serializable
  #44 = Utf8               java/lang/Throwable
  #45 = Utf8               java/lang/System
  #46 = Utf8               out
  #47 = Utf8               Ljava/io/PrintStream;
  #48 = Utf8               java/io/PrintStream
  #49 = Utf8               println
  #50 = Utf8               (I)V

第一、二个常量是不是跟前面16进制文件分析的有一些似曾见过的数字?是一样的,这个比16进制文件看起来更简单明了。

第一个常量是一个方法定义(Methodref),指向了第9和33位置的常量。看9和33位置的常量,又分别指向了另外位置的常量,最后可以拼接成第一个常量右侧的注释内容:// java/lang/Object.""😦)V。这段可以理解为该类的实例构造器的声明,由于Bytecode类没有重写构造方法,所以调用的是父类的构造方法。此处也说明了Bytecode类的直接父类是Object。该方法默认返回值是V,也就是void。

同理分析第二个常量,此处声明了一个字段value,类型为I,I即是int类型。

标识字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型long
S基本类型short
Z基本类型boolean
V特殊类型void
L对象类型,以分号结尾,如Ljava/lang/Object;

对于数组类型,每一位使用一个前置的"[“字符来描述,如定义一个"java.lang.String[] []“类型的维数组,将被记录为”[[Ljava/lang/String;”

常量池可以理解成Class文件中的资源仓库。主要存放字面量和符号引用。字面量类似于Java中的常量概念,如文本字符串、final常量等,不同于C/C++,JVM是在加载Class文件的时候才进行的动态链接,也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当虚拟机运行时,需要从常量池获得对应的符号引用,然后在类创建或运行时解析并翻译到具体的内存地址中。

访问标志

标志名称标志值含义
ACC_PUBLIC0x0001是否为public
ACC_FINAL0x0010是否为final,只有类可以设置
ACC_SUPER0x0020允许使用invokespecial字节码指令
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400abstract类型
ACC_SYNTHETIC0x1000这个类并非由用户代码产生
ACC_ANNOTATION0x2000这是一个注解
**ACC_ENUM0x4000这是一个枚举

常量池后面就是访问标志,用两个字节来表示,标识了类或者接口的访问信息。比如Class文件是类还是接口,是否被定义成public,是否是abstract,是否声明为final等。

这可应该好理解,我们从反编译的字节码中一眼就可以看出,然后对象上表查看。

flags: ACC_PUBLIC, ACC_SUPER

类索引、父类索引、接口索引

访问标志后的两个字节就是类索引。

类索引后的两个字节就是父类索引。

父类索引后的两个字节就是接口索引计数器。

我们找到16进制文件的第33行(为了方便我直接找到了,大家可以从上往下慢慢的整个文件找下来):0100 0428 4929 5600 2100 0300 0900 0100 0a00。其中0003表示类索引,0009表示父类索引,0001表示接口计数器。

前两个索引值都用到了常量池中的值,0003表示第3个位置的值,0009表示第9个位置的值。0001表示有一个接口,0001后面的两个字节000a就是指向的这个接口,000a转换成10进制为10,对应常量池的第10个位置,可以看到这个接口就是java/io/Serializable。

   #3 = Class              #35            // com/doaredo/test/Bytecode
   #9 = Class              #42            // java/lang/Object
  #10 = Class              #43            // java/io/Serializable

字段表

字段表结构

类型名称含义数量
u2access_flags访问标志1
u2name_index字段名索引1
u2descriptor_index描述符索引1
u2attributes_count属性计数器1
attribute_infoattributes属性集合attributes_count

字段表访问标志

标志名称标志值含义
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLATILE0x0040字段是否为volatile
ACC_TRANSTENT0x0080字段是否为transient
ACC_SYNCHETIC0x1000字段是否为由编译器自动产生
ACC_ENUM0x4000字段是否为enum

解读字段表

0a00 0100 0200 0b00 0c00 0000 0400 0100

接口索引后面就是字段表,同样前两个字节表示字段个数,这里接口索引后的两个字节为0001,表示有1个字段,接着两个字节0002,对照上面的访问标志值,可以知道字段为private;接着两个字节000b,表示对应常量池的第11个位置(也就是字段名称);接着000c对应常量池的第12个位置(字段的类型);接着0000表示字段属性,这里属性为0个(只有字段为对象时才可能有属性)。

   #11 = Utf8               value
   #12 = Utf8               I

方法表

方法表结构

类型名称含义数量
u2access_flags访问标志1
u2name_index方法名索引1
u2descriptor_index描述符索引1
u2attributes_count属性计数器1
attribute_infoattributes属性集合attributes_count

方法访问标志

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为public
ACC_PRIVATE0x0002方法是否为private
ACC_PROTECTED0x0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYHCHRONRIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是否是有编译器产生的方法
ACC_VARARGS0x0080方法是否接受参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICTFP0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否是有编译器自动产生的

解读

0a00 0100 0200 0b00 0c00 0000 0400 0100
0d00 0e00 0100 0f00 0000 2f00 0100 0100
0000 052a b700 01b1 0000 0002 0010 0000

同样两个字节0004表示有4个方法,但是我们代码中只有三个方法,这是因为它还包含了默认的构造方法。

我们来分析第一个方法,0001表示方法为public;000d转换成10进制为13,表示方法名在常量池的第13个位置,这个实际上就是默认的构造方法;000e对应常量池的第14个位置;0001表示属性计数器,这里有1个属性,所以后面接着的是一个属性表000f,对应常量池15位置的Code,方法里的代码就是存放在Code属性里的。

  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code

属性表

属性类型

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量池
Deprecated类,方法,字段表被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature类,方法表,字段表用于支持泛型情况下的方法签名
SourceFile类文件记录源文件名称
SourceDebugExtension类文件用于存储额外的调试信息
Synthetic类,方法表,字段表标志方法或字段为编译器自动生成的
LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类,方法表,字段表为动态注解提供支持
RuntimeInvisibleAnnotations表,方法表,字段表用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation方法表作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation方法表作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
AnnotationDefault方法表用于记录注解类元素的默认值
BootstrapMethods类文件用于保存invokeddynamic指令引用的引导方式限定符

属性表结构

类型名称数量含义
u2attribute_name_index1属性名索引
u2attribute_length1属性长度
u1infoattribute_length属性表

部分属性解读

Code属性表

类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2max_stack1操作数栈深度的最大值
u2max_locals1局部变量表所需的存续空间
u4code_length1字节码指令的长度
u1codecode_length存储字节码指令
u2exception_table_length1异常表长度
exception_infoexception_tableexception_length异常表
u2attributes_count1属性集合计数器
attribute_infoattributesattributes_count属性集合
0d00 0e00 0100 0f00 0000 2f00 0100 0100
0000 052a b700 01b1 0000 0002 0010 0000
0006 0001 0000 0005 0011 0000 000c 0001
0000 0005 0012 0013 0000 0021 0014 000e
0001 000f 0000 0039 0003 0001 0000 000b
2a59 b400 0204 60b5 0002 b100 0000 0200

接着上面来分析Code属性,000f对应15个位置,也就是属性名为Code;0000002f这4个字节表示属性长度,这里属性长度转换为10进制就是47,也就是说往后47个字节都是该Code属性的内容;00010001分别表示操作数栈最大深度和局部变量表个数;00000005这4个字节表示字节码指令长度,这里转换成10进制也就是5,表示有5个字节码指令,1个字节码指令占用1个字节,指令如果有参数,则后面字节码就是他的参数,没有参数的话,后面字节码就是下一条指令。我们来解读一下这5个指令:2a b700 01b1,对照文章最后的指令表来看。

  • 2a指令为aload_0,将第0个Slot中为reference类型的本地变量推送到操作数栈顶

  • b7指令为invokespecial,将操作数栈顶的reference类型的数据所指向的对象作为方法接受者,调用此对象的实例构造方法、private方法或者它的父类的方法。后面紧跟着的2个字节即指向其具体要调用的方法。

  • 0001指向常量池中的第1项,这里要调用的就是默认构造方法:

#1 = Methodref          #9.#33         // java/lang/Object."<init>":()V
  • b1指令为return,返回此方法,返回值为void。这条指令执行完,当前方法也就结束了。
  public com.doaredo.test.Bytecode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/doaredo/test/Bytecode;

我们再来看反编译的文件,是不是比16进制文件容易看得多。

0000 052a b700 01b1 0000 0002 0010 0000
0006 0001 0000 0005 0011 0000 000c 0001
0000 0005 0012 0013 0000 0021 0014 000e
0001 000f 0000 0039 0003 0001 0000 000b
2a59 b400 0204 60b5 0002 b100 0000 0200

接着0000表示异常表长度为0,0002表示Code属性表里面还有2个其他的属性表,接着后面就是其他属性的属性表了。0010对应常量表16位置,也就是LineNumberTable,对应反编译的文件我们很容易就看到另外一个属性表就是LocalVariableTable。

LineNumberTable属性表

类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2line_number_table_length1行号表长度
line_number_infoline_number_tableline_number_table_length行号表

line_number_info长度为4个字节,前两个字节表示字节码行号,后两个字节表示Java源代码行号。

0000 052a b700 01b1 0000 0002 0010 0000
0006 0001 0000 0005 0011 0000 000c 0001
0000 0005 0012 0013 0000 0021 0014 000e
0001 000f 0000 0039 0003 0001 0000 000b
2a59 b400 0204 60b5 0002 b100 0000 0200

0010为属性名索引,后面4个字节0000 0006表示长度为6,后面6个字节都是LineNumberTable属性的内容:0001 0000 0005,0001表示有1个行号表;0000 0005表示字节码第0行对应Java源码第7行。

其它属性

Java虚拟机中预定义了20多个属性,只要知道其精髓所在,其他属性都是一个道理。

简单解读法

前面通过对16进制文件的简单解读,需要对照一堆资料才能看得明白,通过javap生成的字节码文件可以很简单的读懂其大致内容。

{
  private int value;
    descriptor: I
    flags: ACC_PRIVATE

  public com.doaredo.test.Bytecode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/doaredo/test/Bytecode;

  public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field value:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field value:I
        10: return
      LineNumberTable:
        line 8: 0
        line 9: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/doaredo/test/Bytecode;
  • 声明一个私有变量value,类型为int,返回类型为int
  • 构造方法Bytecode(),返回值为void,公开方法。 code内的主要属性为:
    • stack-最大操作数栈,JVM运行时会根据这个值来分配栈帧中的操作栈深度
    • locals-局部变量所需的存储空间,单位为Slot,Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字符大小。
    • args_size-方法参数的个数(包括实例方法中的隐藏参数this),显示异常处理器的参数(catch块所定义的异常)。
    • attribute_info-方法体内容,0、1、4为字节码行号,该段代码的意思是将第一个引用类型本地变量推送至栈顶,然后执行该类型的实例方法,也就是常量池存放的第一个变量,也就是注释里的// Method java/lang/Object.""😦)V,然后执行返回语句,结束方法。
    • LineNumberTable-该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用-g:none或-g:lines选项来取消或要求生成这项信息,如果选择不生成,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码的行数来调试程序。
    • LocalVariableTable-描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用-g:none或-g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0、arg1这样的占位符。start表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值