编译后的class文件在Java中保留了多少源码信息?

How much information of the source code does the compiled class file keep in Java?

当我自己的另一个项目的class文件被IntelliJ反编译时 (通过 Fernflower 反编译器), 与反编译代码相比,我对反编译代码的接近程度感到惊讶 源代码, 连方法局部变量名都和源码一样

我不知道 Java 编译过程是如何工作的,并且 JVM 是如何工作的, 我天真的理解是 public 东西的名字 编译后可能需要保留, 但是局部变量的名称, 它们只是便于人类阅读的助记符, 在他们的范围之外完全没用, 而且我认为 JVM 不需要这些信息。

所以, 这些信息是反编译器通过某种魔法简单地弄清楚的吗 还是编译后的 class 保留了很多信息,有什么用?

最终还是要看实际的编译器和具体的编译设置。

如您所述,JVM 本身不需要任何局部变量名。 (严格来说,它也不需要方法名称。甚至可以有 ,但我必须在规范中查找更多关于此的细节才能说得更深刻)。但是 class 文件可以包含超出 JVM 所需信息的附加调试信息。

标准 Java 编译器是 javac。并且文档中已经包含了一些关于可能的调试信息的提示:

-g

Generates all debugging information, including local variables. By default, only line number and source file information is generated.

-g:none

Does not generate any debugging information.

-g:[keyword list]

Generates only some kinds of debugging information, specified by a comma separated list of keywords. Valid keywords are:

  • source : Source file debugging information.
  • lines: Line number debugging information.
  • vars: Local variable debugging information.

可以举个例子试试:

public class ExampleClass {
    public static void main(String[] args) {

        ExampleClass exampleClass = new ExampleClass();
        exampleClass.exampleMethod();
    }

    public void exampleMethod() {
        String string = "This is an example";
        for (int counter = 0; counter < 10; counter++) {
            String localResult = string + counter;
            System.out.println(localResult);
        }
    }
}

编译
javac ExampleClass.java -g:none

将生成一个 class 文件。使用

打印有关此 class 文件的信息
javap -c -v -l ExampleClass.class

(其中-c表示反汇编输出,-v表示输出要冗长,-l表示要打印行号信息),输出如下:

public class ExampleClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #13.#22        // java/lang/Object."<init>":()V
   #2 = Class              #23            // ExampleClass
   #3 = Methodref          #2.#22         // ExampleClass."<init>":()V
   #4 = Methodref          #2.#24         // ExampleClass.exampleMethod:()V
   #5 = String             #25            // This is an example
   #6 = Class              #26            // java/lang/StringBuilder
   #7 = Methodref          #6.#22         // java/lang/StringBuilder."<init>":()V
   #8 = Methodref          #6.#27         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #9 = Methodref          #6.#28         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #10 = Methodref          #6.#29         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #11 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
  #12 = Methodref          #32.#33        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #13 = Class              #34            // java/lang/Object
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               exampleMethod
  #20 = Utf8               StackMapTable
  #21 = Class              #35            // java/lang/String
  #22 = NameAndType        #14:#15        // "<init>":()V
  #23 = Utf8               ExampleClass
  #24 = NameAndType        #19:#15        // exampleMethod:()V
  #25 = Utf8               This is an example
  #26 = Utf8               java/lang/StringBuilder
  #27 = NameAndType        #36:#37        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #28 = NameAndType        #36:#38        // append:(I)Ljava/lang/StringBuilder;
  #29 = NameAndType        #39:#40        // toString:()Ljava/lang/String;
  #30 = Class              #41            // java/lang/System
  #31 = NameAndType        #42:#43        // out:Ljava/io/PrintStream;
  #32 = Class              #44            // java/io/PrintStream
  #33 = NameAndType        #45:#46        // println:(Ljava/lang/String;)V
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/String
  #36 = Utf8               append
  #37 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #38 = Utf8               (I)Ljava/lang/StringBuilder;
  #39 = Utf8               toString
  #40 = Utf8               ()Ljava/lang/String;
  #41 = Utf8               java/lang/System
  #42 = Utf8               out
  #43 = Utf8               Ljava/io/PrintStream;
  #44 = Utf8               java/io/PrintStream
  #45 = Utf8               println
  #46 = Utf8               (Ljava/lang/String;)V
{
  public ExampleClass();
    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

  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 ExampleClass
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method exampleMethod:()V
        12: return

  public void exampleMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: ldc           #5                  // String This is an example
         2: astore_1
         3: iconst_0
         4: istore_2
         5: iload_2
         6: bipush        10
         8: if_icmpge     43
        11: new           #6                  // class java/lang/StringBuilder
        14: dup
        15: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        18: aload_1
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: iload_2
        23: invokevirtual #9                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        26: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: astore_3
        30: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        33: aload_3
        34: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: iinc          2, 1
        40: goto          5
        43: return
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 5
          locals = [ class java/lang/String, int ]
        frame_type = 250 /* chop */
          offset_delta = 37
}

这是相当多的信息,但除了 class 本身的实际 结构 之外没有其他信息。

(你提到必须保留“public东西的名称。但是“私有[=96=的名称] stuff" 也必须保留 - 至少,为了反射。使用像 Class#getDeclaredFields 这样的方法,你仍然可以访问私有字段,例如 - 所以名称必须在某处可用)。


现在,相反的是用

编译它
javac ExampleClass.java -g

保留全部调试信息。如上所述打印结果产生

public class ExampleClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #13.#36        // java/lang/Object."<init>":()V
   #2 = Class              #37            // ExampleClass
   #3 = Methodref          #2.#36         // ExampleClass."<init>":()V
   #4 = Methodref          #2.#38         // ExampleClass.exampleMethod:()V
   #5 = String             #39            // This is an example
   #6 = Class              #40            // java/lang/StringBuilder
   #7 = Methodref          #6.#36         // java/lang/StringBuilder."<init>":()V
   #8 = Methodref          #6.#41         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #9 = Methodref          #6.#42         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #10 = Methodref          #6.#43         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #11 = Fieldref           #44.#45        // java/lang/System.out:Ljava/io/PrintStream;
  #12 = Methodref          #46.#47        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #13 = Class              #48            // java/lang/Object
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               LExampleClass;
  #21 = Utf8               main
  #22 = Utf8               ([Ljava/lang/String;)V
  #23 = Utf8               args
  #24 = Utf8               [Ljava/lang/String;
  #25 = Utf8               exampleClass
  #26 = Utf8               exampleMethod
  #27 = Utf8               localResult
  #28 = Utf8               Ljava/lang/String;
  #29 = Utf8               counter
  #30 = Utf8               I
  #31 = Utf8               string
  #32 = Utf8               StackMapTable
  #33 = Class              #49            // java/lang/String
  #34 = Utf8               SourceFile
  #35 = Utf8               ExampleClass.java
  #36 = NameAndType        #14:#15        // "<init>":()V
  #37 = Utf8               ExampleClass
  #38 = NameAndType        #26:#15        // exampleMethod:()V
  #39 = Utf8               This is an example
  #40 = Utf8               java/lang/StringBuilder
  #41 = NameAndType        #50:#51        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = NameAndType        #50:#52        // append:(I)Ljava/lang/StringBuilder;
  #43 = NameAndType        #53:#54        // toString:()Ljava/lang/String;
  #44 = Class              #55            // java/lang/System
  #45 = NameAndType        #56:#57        // out:Ljava/io/PrintStream;
  #46 = Class              #58            // java/io/PrintStream
  #47 = NameAndType        #59:#60        // println:(Ljava/lang/String;)V
  #48 = Utf8               java/lang/Object
  #49 = Utf8               java/lang/String
  #50 = Utf8               append
  #51 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #52 = Utf8               (I)Ljava/lang/StringBuilder;
  #53 = Utf8               toString
  #54 = Utf8               ()Ljava/lang/String;
  #55 = Utf8               java/lang/System
  #56 = Utf8               out
  #57 = Utf8               Ljava/io/PrintStream;
  #58 = Utf8               java/io/PrintStream
  #59 = Utf8               println
  #60 = Utf8               (Ljava/lang/String;)V
{
  public ExampleClass();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LExampleClass;

  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 ExampleClass
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method exampleMethod:()V
        12: return
      LineNumberTable:
        line 4: 0
        line 5: 8
        line 6: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            8       5     1 exampleClass   LExampleClass;

  public void exampleMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: ldc           #5                  // String This is an example
         2: astore_1
         3: iconst_0
         4: istore_2
         5: iload_2
         6: bipush        10
         8: if_icmpge     43
        11: new           #6                  // class java/lang/StringBuilder
        14: dup
        15: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        18: aload_1
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: iload_2
        23: invokevirtual #9                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        26: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: astore_3
        30: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        33: aload_3
        34: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: iinc          2, 1
        40: goto          5
        43: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 11
        line 12: 30
        line 10: 37
        line 14: 43
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           30       7     3 localResult   Ljava/lang/String;
            5      38     2 counter   I
            0      44     0  this   LExampleClass;
            3      41     1 string   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 5
          locals = [ class java/lang/String, int ]
        frame_type = 250 /* chop */
          offset_delta = 37
}
SourceFile: "ExampleClass.java"

主要区别是

  • class包含源文件名
  • 常量池有 许多 个条目
  • 这些方法包含一个 LineNumberTable 和一个 LocalVariableTable

例如,考虑 exampleMethod():

LineNumberTable:
  line 9: 0
  line 10: 3
  line 11: 11
  line 12: 30
  line 10: 37
  line 14: 43
LocalVariableTable:
  Start  Length  Slot  Name   Signature
     30       7     3 localResult   Ljava/lang/String;
      5      38     2 counter   I
      0      44     0  this   LExampleClass;
      3      41     1 string   Ljava/lang/String;

有关这些属性结构的详细信息在 LineNumberTable and the LocalVariableTable 的文档中给出。

对于LineNumberTable,它表示

It may be used by debuggers to determine which part of the code array corresponds to a given line number in the original source file.

对于LocalVariableTable,它表示

It may be used by debuggers to determine the value of a given local variable during the execution of a method.

javap的输出中,局部变量的names已经解析。然而,table本身包含的实际信息只是常量池中的索引(这就是为什么它在保留调试信息时具有更多条目的原因)。例如,localResult 变量的条目显示为

     30       7     3 localResult   Ljava/lang/String;

虽然它实际上只包含对条目的引用

  #27 = Utf8               localResult

常量池的


So, are these information simply figured out by the de-compiler through some magic or dose the compiled class retain a lot of information and what for?

如上所示,编译后的class可以保留很多信息。毕竟,IDE 的主要目的之一是为 调试器 提供一个漂亮的可视化界面。因此,大多数由 IDE 以一种或另一种方式触发的编译器将默认尝试保留尽可能多的调试信息。