java - 编译后的class文件在Java中保留了多少源代码信息?
问题描述
当我自己的另一个项目的一个类文件被IntelliJ(通过Fernflower反编译器)反编译时,我惊叹于反编译的代码与源代码相比如此接近,甚至方法局部变量名称都与原始相同源代码。
我对Java编译过程如何工作以及JVM如何工作一无所知,我的幼稚理解是编译后可能需要保留公共东西的名称,但局部变量的名称,它们只是助记符人类阅读,在他们的范围之外完全没用,我认为 JVM 不需要这些信息。
那么,这些信息是由反编译器通过某种魔法简单地计算出来的,还是编译后的类保留了很多信息,有什么用?
解决方案
最后,这取决于实际的编译器和确切的编译设置。
正如您所指出的,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"
主要区别是
- 该类包含源文件名
- 常量池有更多条目
- 方法包含 a
LineNumberTable
和 aLocalVariableTable
。
例如,考虑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 以一种或另一种方式触发的编译器将默认尝试保留尽可能多的调试信息。
推荐阅读
- javascript - 输入文件过滤允许加载多页pdf
- javascript - 如何使用类和函数为 html5-canvas 上的多个对象设置动画?
- scala - 优化 Flink 转换
- javascript - 在 javascript 中导入文件总是在路径前面添加 localhost
- jmeter - JMeter 负载测试异步 api
- amazon-web-services - 如何在 NodeJS 中使用 AWS SES 发送附件?
- xamarin.forms - 从点数组中检索签名
- opencart - 我如何在 opencart 文件中搜索代码行以在类别页面 OpenCart 2.3 中更改网格视图
- java - Selenium - 单独通过,TestSuite 没有
- python-requests - 在 Python 请求库的 Header 中使用 JSON