首页 > 解决方案 > 编译后的class文件在Java中保留了多少源代码信息?

问题描述

当我自己的另一个项目的一个类文件被IntelliJ(通过Fernflower反编译器)反编译时,我惊叹于反编译的代码与源代码相比如此接近,甚至方法局部变量名称都与原始相同源代码。

我对Java编译过程如何工作以及JVM如何工作一无所知,我的幼稚理解是编译后可能需要保留公共东西的名称,但局部变量的名称,它们只是助记符人类阅读,在他们的范围之外完全没用,我认为 JVM 不需要这些信息。

那么,这些信息是由反编译器通过某种魔法简单地计算出来的,还是编译后的类保留了很多信息,有什么用?

标签: javabytecode.class-file

解决方案


最后,这取决于实际的编译器和确切的编译设置。

正如您所指出的,JVM 本身不需要任何局部变量名称。(严格来说,它也不需要方法名。甚至可以有两个具有相同名称和参数但返回类型不同的方法,但我必须在规范说得更深刻)。但是类文件可以包含超出 JVM 所需信息的附加调试信息。

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

-G

生成所有调试信息,包括局部变量。默认情况下,只生成行号和源文件信息。

-g:无

不生成任何调试信息。

-g:[关键字列表]

仅生成某些类型的调试信息,由逗号分隔的关键字列表指定。有效的关键字是:

  • source:源文件调试信息。
  • lines:行号调试信息。
  • vars:局部变量调试信息。

可以用一个例子来试试这个:

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

将生成一个类文件。打印有关此类文件的信息

javap -c -v -l ExampleClass.class

(where-c表示反汇编输出,-v表示输出要verbose,-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#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"

主要区别是

  • 该类包含源文件名
  • 常量池有更多条目
  • 方法包含 aLineNumberTable和 a 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和的文档中给出LocalVariableTable

对于LineNumberTable,它说

调试器可以使用它来确定代码数组的哪一部分对应于原始源文件中的给定行号。

对于LocalVariableTable,它说

调试器可以使用它来确定方法执行期间给定局部变量的值。

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

     30       7     3 localResult   Ljava/lang/String;

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

  #27 = Utf8               localResult

的常量池。


那么,这些信息是由反编译器通过某种魔法简单地计算出来的,还是编译后的类保留了很多信息呢?

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


推荐阅读