字节码文件(.class)内容解析

2021-11-08  本文已影响0人  Wannay

我一直对.class文件编译之后的内容比较感兴趣,我们现在一起来揭秘吧!

我们编写如下的测试代码,我们想要去看看编译出来的.class文件中究竟包含了什么?

package com.wanna.jdkset.TestString;

public class TestString {
    public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("wanna");
    }
}

这里需要借用到IDEA工具中的jclasslib插件,或者是javap命令去进行反编译。

我们用到的命令是javap -v com.wanna.jdkset.TestString,去进行编译成为字节码,最终得到如下的信息

public class com.wanna.jdkset.TestString
  minor version: 0
  major version: 55
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#23         // java/lang/Object."<init>":()V
   #2 = Class              #24            // java/lang/StringBuffer
   #3 = Methodref          #2.#23         // java/lang/StringBuffer."<init>":()V
   #4 = String             #25            // wanna
   #5 = Methodref          #2.#26         // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   #6 = Class              #27            // com/wanna/jdkset/TestString
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/wanna/jdkset/TestString;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               buffer
  #20 = Utf8               Ljava/lang/StringBuffer;
  #21 = Utf8               SourceFile
  #22 = Utf8               TestString.java
  #23 = NameAndType        #8:#9          // "<init>":()V
  #24 = Utf8               java/lang/StringBuffer
  #25 = Utf8               wanna
  #26 = NameAndType        #29:#30        // append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
  #27 = Utf8               com/wanna/jdkset/TestString
  #28 = Utf8               java/lang/Object
  #29 = Utf8               append
  #30 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuffer;
{
  public com.wanna.jdkset.TestString();
    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 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wanna/jdkset/TestString;

  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           #2                  // class java/lang/StringBuffer
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuffer."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String wanna
        11: invokevirtual #5                  // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
        14: pop
        15: return
      LineNumberTable:
        line 18: 0
        line 19: 8
        line 20: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            8       8     1 buffer   Ljava/lang/StringBuffer;
}

我们来看开头的一部分

  minor version: 0
  major version: 55
  flags: ACC_PUBLIC, ACC_SUPER

minjor和major这两个主要是和字节码版本相关的。flags主要就是类的修饰符,ACC_SUPER代表这个类有父类,ACC_PUBLIC代表这个类是public修饰的。

我们先截取常量池的一部分,其它暂时不看

   #1 = Methodref          #7.#23         // java/lang/Object."<init>":()V
   #2 = Class              #24            // java/lang/StringBuffer
   #3 = Methodref          #2.#23         // java/lang/StringBuffer."<init>":()V
   #4 = String             #25            // wanna
   #5 = Methodref          #2.#26         // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   #6 = Class              #27            // com/wanna/jdkset/TestString
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/wanna/jdkset/TestString;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               buffer
  #20 = Utf8               Ljava/lang/StringBuffer;
  #21 = Utf8               SourceFile
  #22 = Utf8               TestString.java
  #23 = NameAndType        #8:#9          // "<init>":()V
  #24 = Utf8               java/lang/StringBuffer
  #25 = Utf8               wanna
  #26 = NameAndType        #29:#30        // append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
  #27 = Utf8               com/wanna/jdkset/TestString
  #28 = Utf8               java/lang/Object
  #29 = Utf8               append
  #30 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuffer;

我们可以看到,常量池中的Class的部分主要包括下面这几个,它主要存储的就是我们的Class的相关信息。

   #2 = Class              #24            // java/lang/StringBuffer
   #6 = Class              #27            // com/wanna/jdkset/TestString
   #7 = Class              #28            // java/lang/Object

我们可以看到,这三个Class分别指向了#24#27以及#28,我们再从常量池中找到这三个内容,我们发现它们最终都是Utf8的字符串,而这些字符串的内容就是我们会用到的相关类的全限定名。(其实就是注释中的内容,这应该是javap工具帮我们做的)

我们再来查看Methodref部分,主要包括如下几个

   #1 = Methodref          #7.#23         // java/lang/Object."<init>":()V
   #3 = Methodref          #2.#23         // java/lang/StringBuffer."<init>":()V
   #5 = Methodref          #2.#26         // java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;

第一个Methodref就是#7.#23,查找常量池可以发现它的内容就是java/lang/Object."<init>":()V,我们首先观察这个内容?

我们可以发现构造器方法,其实本质上也只是一个方法罢了,只不过方法名叫<init>

第二个Methodref是#2.#23,这个和上一个Methodref很类似。
第三个Methodref是#2.#26,查看常量池可以发现它的内容是java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;,我们也是进行分解:

下面要看的就是方法对应的字节码了

  public com.wanna.jdkset.TestString();
    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 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wanna/jdkset/TestString;

我们可以看到方法名是com.wanna.jdkset.TestString,方法的描述符是()V,访问标识符是ACC_PUBLIC

stack=1, locals=1, args_size=1说明栈的最大为1,局部变量表的大小是1,参数数量是1。

下面就是方法的主要执行部分的字节码了

         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

先通过aload_0,把局部变量表中的0号槽位的内容(this)压入栈中,然后使用invokespecial #1去执行Object类的空参构造器方法,然后使用return将构造器的栈帧销毁,回到调用方法的栈帧。

然后就是行号表(建立的是字节码的行号和java代码的行号之间的对应关系)

      LineNumberTable:
        line 9: 0

然后是局部变量表

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wanna/jdkset/TestString;

我们可以看到,局部变量表中居然放了个this对象?其实所有非静态方法的局部变量表中的0号槽位(slot)的元素都是this,相当于我们所有非静态方法的第一个参数都是this(你也可以在第一个参数位置写个this,但是你不写编译器帮你做了)。

比如下面两种方式都是完全等价的,如果你写了this,编译器就不会再帮你做了,肯定会往局部变量表的槽位0中放入this,如果你没写,那么编译器自然会帮你完成。

    // method01:
    public void test(Test this, String str) {

    }
    // method02:
    public void test(String str) {

    }

接着来看main方法的字节码

  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           #2                  // class java/lang/StringBuffer
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuffer."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String wanna
        11: invokevirtual #5                  // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
        14: pop
        15: return
      LineNumberTable:
        line 18: 0
        line 19: 8
        line 20: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            8       8     1 buffer   Ljava/lang/StringBuffer;

首先我们可以直观的看到,局部变量表的槽位0位置放的不是this,而是args,槽位1中则是放的就是buffer的引用,其它就没什么说的。

上一篇下一篇

猜你喜欢

热点阅读