Java字节码:本地变量的类型?

10
根据这篇文章http://slurp.doc.ic.ac.uk/pubs/observing/linking.html#assignment所述:
由于Java代码和字节码之间的信息差异(字节码不包含局部变量的类型),因此验证器不需要检查局部变量或参数的赋值子类型。
我的问题是:为什么字节码不包含局部变量的类型信息,而确实包含参数和返回值的类型信息?

2
我怀疑类型值在“编译时”得到了检查,结果代码不再需要具有类型值信息,只需进行二进制数据处理。然而,方法参数和返回值是在“运行时”调用的,需要类型检查的信息。 - recursion.ninja
1
因为事情就是这样。JDK验证器使用数据流分析来查找每个数据使用到达的类型。参数和实例/静态变量需要声明类型以输入数据流。另一方面,计算结果的类型可以通过规则确定。此外,编译时局部变量经常消失为堆栈值。 - Hot Licks
3个回答

6
首先,有几种不同的类型概念。编译时类型包括泛型。但是,泛型在编译后不存在。
变量的静态类型可以是int、float、long、double、returnaddress或对象引用。对象引用还带有上限类型,以便所有引用都是java/lang/String的子类型。字段还可以具有byte、short、char或boolean等短类型。它们在执行目的上与int相同,但存储不同。
最后,运行时类型与已验证的静态类型相同,但在对象引用的情况下,表示被引用实例的实际类型。请注意,由于验证器的惰性,有些情况下运行时类型实际上可能不是已验证类型的子类型。例如,在Hotspot中,声明类型为Comparable的变量实际上可以保存任何对象,因为VM在验证时不检查接口。
除了反射和调试的可选属性外,编译时信息不会被保留。这是因为没有理由保留它。
局部变量没有显式类型信息(除了新的StackMapTable属性,但那只是技术细节)。相反,当类加载时,字节码验证器通过运行静态数据流分析为每个值推断类型。其目的不是像编译时类型检查那样捕获错误,因为假定字节码已经通过了这样的检查。
相反,验证的目的是确保指令对VM本身不危险。例如,它需要确保您不会将整数作为对象引用来解释,因为那可能导致任意内存访问和黑客攻击VM。
因此,虽然字节码值没有显式类型信息,但它们具有静态类型推断的隐式类型。这些细节基于每个VM的内部实现细节而有所不同,尽管它们应该遵循JVM标准。但是,您只需要在手写字节码中担心这个问题。
字段具有显式类型,因为VM需要知道其中存储的数据类型。方法参数和返回类型编码在所谓的方法描述符中,也用于类型检查。它们无法自动推断,因为这些值可以来自或去往任何地方,而类型检查是按类进行的。
补充说明一下,当谈到验证类型时,我省略了一些细节。对象类型还跟踪它们是否已初始化,以及如果未初始化,则创建它们的指令。地址类型跟踪创建它们的jsr的目标。

值得注意的是,returnAddress 类型仅与 jsrjsr_w 操作码配合使用,这些操作码在 Java 6 中已被禁止。现代字节码处理的值永远不会包括 returnAddress - Tom Anderson
@Tom 它们不能在主版本号高于49的类文件中使用。但是您仍然可以在现代VM上运行低版本号的classfiles。如果您愿意,您可以将版本设置为45.0,这样做将启用Hotspot中一些有趣的未记录行为。如果您只考虑Java编译的字节码,则javac生成的字节码永远不会执行相当多的操作。但对于字节码黑客来说,它仍然很有趣。 - Antimony
真的!我们必须保留这些过时的东西,这有点可惜,因为它使得解释字节码的工作变得更加复杂。 - Tom Anderson
1
@Tom 更有趣的是,如果你查看源代码,会发现一些代码似乎是在 Java 被称为 Java 之前留下的。即使是 Hotspot 开发人员也不确定它为什么还在那里。 - Antimony
@Antimony非常有用,读起来很愉快!添加一些参考资料将使这个答案更加完美! - gaborsch

3

这是一篇相当古老的论文。当前的类文件确实包括本地变量和堆栈变量的类型。这些类型并不存储在方法字节码中,而是存储在附加到方法的StackMapTable属性中。

通过数据流分析可以重构所有本地变量和堆栈元素的类型,即使没有StackMapTable,但这是计算上昂贵的。带有StackMapTable的代码可以更快地进行验证。尽管我必须承认,我不知道验证StackMapTable如何比进行分析更快,但我对此几乎一无所知。


我很钦佩你的专业知识。对我来说,字节码就像魔法一样……同样神秘莫测。 - scottb
我并没有真正的专业知识——我只是阅读了规格说明而已。 - Tom Anderson
StackMapTables只为每个基本块开头的值提供类型,因此需要数据流分析来重构所有值的类型。特别地,线性代码甚至可能没有StackMapTable属性。它之所以更快,是因为在循环或分支的情况下,数据流不再需要进行多次通过。 - Antimony
啊,只是每个基本块吗?那样的话,确实会有一些类型未被覆盖的变量。 - Tom Anderson

2

Java bytecode 保留有关字段方法返回值参数的类型信息,但是它不包含局部变量的类型信息,正如您所要求的那样。

Java类文件中的类型信息使得反编译bytecode比反编译机器代码更容易。因此,反编译Java bytecode需要分析大多数局部变量类型,扁平化基于堆栈的指令并结构化循环条件语句。然而,字节码反编译的任务比编译要难得多。您会经常看到反编译器无法完全执行其预期功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接