创buildJava类文件是确定性的吗?

当使用相同的JDK (即相同的javac可执行文件)时,生成的类文件是否始终相同? 根据操作系统硬件的不同,有没有区别? 除JDK版本外,是否还有其他因素会导致差异? 是否有任何编译器选项可以避免差异? 在理论上只是可能有区别,或者Oracle的javac实际上为相同的input和编译器选项生成不同的类文件?

更新1我感兴趣的一代 ,即编译器输出,而不是一个类文件是否可以在各种平台上运行

更新2 “相同的JDK”,我也是指相同的javac可执行文件。

更新3 Oracle编译器理论差异与实际差异的区别。

[编辑,添加解释的问题]
“在不同的平台上运行相同的javac可执行文件会产生不同的字节码的情况是怎样的?

让我们这样说:

我可以很容易地生成一个完全一致的Java编译器,在给定相同的.java文件的情况下,它永远不会生成两次相同的.class文件。

我可以通过调整各种字节码结构或简单地将多余的属性添加到我的方法(这是允许的)来做到这一点。

鉴于规范要求编译器产生逐字节的相同的类文件,我会避免这样的结果。

然而 ,我检查过几次,使用相同的开关(和相同的库!)编译相同的源文件的结果是相同的.class文件。

更新:我最近偶然发现了这个有趣的博客文章,讲述了在Java 7中实现switch String 。 在这个博客文章中,有一些相关的部分,我将在这里引用(重点是我的):

为了使编译器的输出具有可预测性和可重复性,这些数据结构中使用的映射和集合是LinkedHashMapLinkedHashSet而不仅仅是HashMapsHashSets给定编译期间生成的代码的function正确性方面使用HashMapHashSet会很好 ; 迭代顺序无关紧要。 但是, 我们发现javac的输出不会因为系统类的实现细节而有所不同

这很清楚地说明了这个问题:只要符合规范,编译器不需要以确定的方式进行操作。 然而,编译器开发人员意识到, 尝试一下是个好主意(可能它并不是太昂贵)。

编译器没有义务在每个平台上生成相同的字节码。 您应该咨询不同供应商的javac实用程序以获得具体的答案。


我将通过文件sorting显示一个实际的例子。

假设我们有两个jar文件: my1.jarMy2.jar 。 它们放在lib目录中,并排放置。 编译器按字母顺序读取它们(因为这是lib ),但是当文件系统不区分大小写时,顺序是my1.jarMy2.jar ,而如果区分大小写,顺序是my1.jarMy2.jar

my1.jar有一个类A.class和一个方法

 public class A { public static void a(String s) {} } 

My2.jar具有相同的A.class ,但具有不同的方法签名(接受Object ):

 public class A { public static void a(Object o) {} } 

很明显,如果你有一个电话

 String s = "x"; Aa(s); 

它会在不同的情况下编译一个不同签名的方法调用。 所以, 根据您的文件系统区分大小写,您将得到不同的类。

简答 – NO


长答案

他们的bytecode对于不同的平台不一定是一样的。 这是JRE(Java运行时环境),它知道如何执行字节码。

如果你仔细阅读Java VM规范,你会发现,不同平台的字节码是一样的。

通过类文件格式 ,它显示了一个类文件的结构

 ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } 

检查一下小版本和主版本

minor_version,major_version

minor_version和major_version项的值是该类文件的次版本号和主版本号。一个主版本号和一个次版本号决定了类文件格式的版本。 如果一个类文件的主版本号为M,次版本号为m,那么我们将它的类文件格式的版本表示为Mm。因此,类文件格式版本可以按照字典顺序排列,例如1.5 <2.0 <2.1。 Java虚拟机实现可以支持版本v的类文件格式,当且仅当v处于某个连续范围Mi.0或Mj.m. 只有Sun可以指定符合Java平台特定版本级别的Java虚拟机实现可支持的版本范围

通过脚注阅读更多内容

1 Sun的JDK版本1.0.2的Java虚拟机实现支持从45.0到45.3(含)的类文件格式。 Sun的JDK版本1.1.X可以支持从45.0到45.65535(含)的版本的类文件格式。 Java 2平台的版本1.2的实现可以支持范围从45.0到46.0的版本的类文件格式。

因此,调查所有这一切表明,不同平台上生成的类文件不需要相同。

首先,规范中绝对没有这样的保证。 符合的编译器可以将编译时间标记为生成的类文件作为附加(自定义)属性,并且类文件仍然是正确的。 然而,它会在每一个版本上产生一个字节级别的不同文件,并且很平常。

其次,即使没有这样的讨厌的技巧,没有理由期望编译器连续两次做同样的事情,除非它的configuration和input在两种情况下是相同的。 规范确实将源文件名称描述为标准属性之一,将空行添加到源文件中可以更改行号表。

第三,由于主机平台的原因,我从来没有遇到过构build上的任何差异(除了归因于类path上的不同之处)。 基于平台(即,本地代码库)而变化的代码不是类文件的一部分,并且在加载类之后从字节代码实际生成本地代码。

第四(也是最重要的一点),它想要知道这一点,它会产生恶劣的过程气味 (如代码味道,但是对于代码的行为)。 在可能的情况下版本化源代码,而不是构build版本,如果您确实需要在整个组件级别而不是单个类文件上对版本进行版本化。 首选使用CI服务器(如Jenkins)来pipe理将源代码转换为可运行代码的过程。

我相信,如果使用相同的JDK,生成的字节码将始终是相同的,与所使用的硬件和操作系统无关。 字节码生成由java编译器完成,该编译器使用确定性algorithm将源代码“转换”为字节码。 所以,输出总是一样的。 在这些情况下,只有源代码的更新会影响输出。

总的来说,我不得不说,不能保证相同的源代码在由相同的编译器编译时会产生相同的字节码,但在不同的平台上。

我会研究涉及不同语言(代码页)的情况,例如Windows支持日语。 考虑多字节字符; 除非编译器总是假定它需要支持所有可能针对8位ASCII进行优化的语言。

Java语言规范中有关于二进制兼容性的一节。

在SOM中发布到发布二进制兼容性框架(Forman,Conner,Danforth和Raper,OOPSLA'95会议logging)中,Java编程语言二进制文件在作者识别的所有相关转换下是二进制兼容的(有一些注意事项尊重添加实例variables)。 使用他们的scheme,下面列出了Java编程语言支持的一些重要的二进制兼容更改:

重新实现现有的方法,构造函数和初始化方法以提高性能。

•更改方法或构造函数以返回它们之前通过进入无限循环或导致死锁通常不应该发生或失败的exception的input返回值。

•将新的字段,方法或构造函数添加到现有的类或接口。

•删除类的私有字段,方法或构造函数。

•更新整个包时,删除包中的默认(仅包)访问字段,方法或类和接口的构造函数。

•重新sorting现有types声明中的字段,方法或构造函数。

•在类层次结构中向上移动一个方法。

重新sorting类或接口的直接超接口列表。

在types层次结构中插入新的类或接口types。

本章规定了所有实现保证的二进制兼容性的最低标准。 Java编程语言保证兼容性,当类和接口的二进制文件混合在一起时,这些二进制文件并不知道来自兼容源,但是其源以这里描述的兼容方式进行了修改。 请注意,我们正在讨论应用程序版本之间的兼容性。 关于Java SE平台发行版之间兼容性的讨论超出了本章的范围。

Java allows you write/compile code on one platform and run on different platform. AFAIK ; 这只有在不同平台上生成的类文件相同或技术上相同时才可能。

编辑

我的意思是技术上相同的意见是。 如果逐字节比较,则不需要完全相同。

所以根据规范,不同平台上的一个类的.class文件不需要逐字节匹配。

对于这个问题:

“在不同的平台上运行相同的javac可执行文件会产生不同的字节码的情况是什么?

交叉编译示例显示了我们如何使用Javac选项:-target版本

该标志生成与我们在调用此命令时指定的Java版本兼容的类文件。 因此,类文件将根据我们在使用此选项进行的编译期间提供的属性而有所不同。

最有可能的答案是“是”,但要得到确切的答案,在编译期间需要search一些键或GUID代。

我不记得发生这种情况。 例如,为了序列化目的而使用ID,它是硬编码的,即由程序员或IDE生成。

PS JNI也可以。

PPS我发现javac本身是用java编写的。 这意味着它在不同的平台上是相同的。 因此,没有理由就不会生成不同的代码。 所以,只能使用本地电话才能做到这一点。

有两个问题。

 Can there be a difference depending on the operating system or hardware? 

这是一个理论上的问题,答案显然是的, 可以的 。 正如其他人所说,规范不要求编译器产生逐字节的相同的类文件。

即使现在的每个编译器在所有情况下(不同的硬件等)都生成相同的字节码,明天的答案可能会不同。 如果您从不计划更新javac或您的操作系统,则可以在特定情况下testing该版本的行为,但是如果从Java 7 Update 11更改为Java 7 Update 15,结果可能会有所不同。

 What are the circumstances where the same javac executable, when run on a different platform, will produce different bytecode? 

这是不可知的。

我不知道configurationpipe理是否是你提出问题的理由,但这是一个可以理解的理由。 比较字节码是一个合法的IT控制,但只是为了确定类文件是否改变,而不是确定源文件是否确实。

我会换一种方式。

首先,我认为这个问题不是关于确定性的:

当然这是确定性的:在计算机科学中很难实现随机性,编译器没有理由在这里介绍它。

其次,如果通过“同一个源代码文件的字节码文件有多相似?”来重新编写它,那么不能依赖于它们会相似的事实。

确保这一点的一个好办法是将.class(或.pyc)放在你的git阶段。 你会意识到,在你的团队中的不同计算机之间,当没有对.py文件进行更改(并且重新编译.pyc)时,git会通知.pyc文件之间的变化。

至less这就是我所观察到的。 所以把* .pyc和* .class放在你的.gitignore中!