我是硕士生,正在研究静态分析。
在我的一次测试中,我遇到了在 java 编译器中标记行的问题。
我有以下java代码:
226: String json = "/org/elasticsearch/index/analysis/commongrams/commongrams_query_mode.json";
227: Settings settings = Settings.settingsBuilder()
228: .loadFromStream(json, getClass().getResourceAsStream(json))
229: .put("path.home", createHome())
230: .build();
编译此代码并执行命令 javap -p -v CLASSNAME
时,我得到一个表格,其中包含字节码中每条指令对应的源代码行。见下图:
Bytecode table
问题是在调用
.put (" path.home ", createHome ())
方法,字节码基本上生成四个指令:19: anewarray
24: ldc - String path.home
30: invokespecial - createHome
34: invokevirtual - put
前两个标记为第 228 行(错误),后两个标记为第 229 行(正确)。见下图:
Bytecode table
这是
.put("path.home", createHome())
的原始实现方法: public Builder put(Object... settings) {
if (settings.length == 1) {
// support cases where the actual type gets lost down the road...
if (settings[0] instanceof Map) {
//noinspection unchecked
return put((Map) settings[0]);
} else if (settings[0] instanceof Settings) {
return put((Settings) settings[0]);
}
}
if ((settings.length % 2) != 0) {
throw new IllegalArgumentException("array settings of key + value order doesn't hold correct number of arguments (" + settings.length + ")");
}
for (int i = 0; i < settings.length; i++) {
put(settings[i++].toString(), settings[i].toString());
}
return this;
}
我已经尝试使用 Oracle-JDK v8 和 Open-JDK v16 编译代码,并且都得到了结果。我还通过更改
put()
进行了测试。方法通过删除其参数。编译此代码时,未出现标记行的问题。我想知道为什么字节码指令映射行
229: .put (" path.home ", createHome ())
在 java 源代码中的原始行以外的行上?有谁知道这是故意的吗?
最佳答案
这是连接的方式,行号关联存储在类文件和javac
的历史记录中。编译器。
line number table仅包含将行号与标记其开头的代码位置相关联的条目。因此,假定该位置之后的所有指令都属于表中明确提及的下一个位置之前的同一行。
由于详细信息会占用空间,并且规范不要求行号表具有特定的精度,因此编译器供应商对包含哪些详细信息做出了不同的决定。
过去,即直到 Java 7,javac
只为语句的开头生成了行号表条目,所以当我用 Java 7 的 javac
编译以下代码时
String settings = new StringBuilder() // this is line 7 in my .java file
.append('a')
.append(
5
+
"".length())
.toString();
我得到类似的东西stack=3, locals=2, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: bipush 97
9: invokevirtual #4 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
12: iconst_5
13: ldc #5 // String
15: invokevirtual #6 // Method java/lang/String.length:()I
18: iadd
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_1
26: return
LineNumberTable:
line 7: 0
line 14: 26
这将导致属于该语句的所有指令仅与第 7 行相关联。这已经被认为太少了,所以从Java 8开始,
javac
为跨越多行的表达式中的方法调用生成额外的条目。因此,当我使用 Java 8 或更高版本编译相同的代码时,我得到stack=3, locals=2, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: bipush 97
9: invokevirtual #4 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
12: iconst_5
13: ldc #5 // String
15: invokevirtual #6 // Method java/lang/String.length:()I
18: iadd
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_1
26: return
LineNumberTable:
line 7: 0
line 8: 9
line 12: 15
line 9: 19
line 13: 22
line 14: 26
请注意每个附加条目(与 Java 7 版本相比)如何指向调用指令,以确保方法调用与正确的行号相关联。这极大地改进了异常堆栈跟踪以及步骤调试。没有显式条目的非调用指令仍将与它们最近的具有条目的前面代码位置相关联。
因此,
bipush 97
'a'
对应的指令常量与第 7 行相关联,因为只有随后的 append
消耗常量的调用有一个显式条目将它与第 8 行相关联。下一个表达式的后果,
5 + "".length()
,更是戏剧化。推送常量的说明,
iconst_5
和 ldc [""]
, 关联到第 8 行,前一个 append
的位置调用,而 iadd
指令,实际上属于+
5
之间的运算符和 ""
常量,与第 12 行相关联,因为最近获得显式行号的调用指令是 length()
调用。为了比较,Eclipse 是这样编译相同代码的:
stack=3, locals=2, args_size=1
0: new #20 // class java/lang/StringBuilder
3: dup
4: invokespecial #22 // Method java/lang/StringBuilder."<init>":()V
7: bipush 97
9: invokevirtual #23 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
12: iconst_5
13: ldc #27 // String
15: invokevirtual #29 // Method java/lang/String.length:()I
18: iadd
19: invokevirtual #35 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #38 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_1
26: return
LineNumberTable:
line 6: 0
line 7: 7
line 9: 12
line 11: 13
line 9: 18
line 8: 19
line 12: 22
line 6: 25
line 13: 26
Eclipse 编译器没有 javac
的历史,而是首先被设计为为表达式生成行号条目。我们可以看到它将属于调用表达式的第一条指令(不是调用指令)与右侧的行相关联,即 bipush 97
为 append('a')
和 ldc [""]
为 "".length()
.此外,它还有
iconst_5
的附加条目。 , iadd
, 和 astore_1
, 将它们与正确的行相关联。当然,这种更高的精度也会导致类文件稍大。
关于java - 在java编译器中标记行的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67409058/