Java类文件的创建过程是确定性的吗?

96
当使用相同的JDK(即相同的javac可执行文件)时,生成的类文件是否总是相同的?根据操作系统或硬件不同可能有差异吗?除了JDK版本,是否有其他因素会导致差异?是否有编译器选项可以避免差异?在相同的输入和编译器选项下,Oracle的javac实际上会生成不同的类文件吗?
更新1:我对生成的编译器输出感兴趣,而不是类文件是否可以在各种平台上运行。
更新2:通过“相同的JDK”,我也指的是相同的javac可执行文件。
更新3:区分Oracle编译器中理论差异和实际差异。
编辑,添加重新表述的问题: “在哪些情况下,相同的javac可执行文件在不同的平台上运行会产生不同的字节码?”

5
CORA并不意味着在不同平台上编译时生成的字节码完全相同,它仅表示所生成的字节码将执行完全相同的操作。 - Sergey Kalinichenko
11
你为什么关心呢?这听起来像是一个"XY 问题"。具体信息请参考链接:http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem。 - Joachim Sauer
4
考虑是否对二进制文件进行版本控制 - 你可能只希望在源代码发生更改时才检测变化,但如果JDK可以随意更改输出的二进制文件,那么这不是一个明智的想法。 - RB.
7
编译器可以生成任何符合规范的字节码,来表示编译后的代码。事实上,一些编译器更新修复了产生稍有不同代码(通常具有相同运行时行为)的 bug。换句话说:如果你想检测源代码的变化,请检查源代码的更改。 - Joachim Sauer
3
@dasblinkenlight:你假设他们声称的答案实际上是正确和最新的(这是值得怀疑的,因为这个问题来自于2003年)。 - Joachim Sauer
显示剩余8条评论
11个回答

70

这么说吧:

我可以轻松地制作一个完全符合规范的Java编译器,其在给定相同的.java文件时永远不会产生相同的.class文件。

我可以通过调整所有类型的字节码构造或简单地向我的方法添加多余的属性(这是允许的)来实现这一点。

鉴于规范并没有要求编译器生成按字节完全相同的类文件,因此我会避免依赖这样的结果。

然而,我检查过几次,使用相同的编译器、相同的开关(以及相同的库!)编译相同的源文件确实会产生相同的.class文件。

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

  

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

下面的内容非常清楚地说明了这个问题:只要编译器符合规范,它就不需要以确定性的方式运行。然而,编译器开发人员意识到,通常情况下尽量尝试(如果代价不太高的话)是一个好主意。


@gaborsch 缺少什么?“在哪些情况下,同一 javac 可执行文件在不同平台上运行时会产生不同的字节码?”基本上取决于编译器生产组的心血来潮。 - emory
3
对我来说,这已经足够的理由不依赖它:如果我依赖于编译器始终产生相同的代码这一事实,更新的JDK可能会破坏我的构建/归档系统。 - Joachim Sauer
3
@GaborSch:你已经有了一个完全合适的例子来说明这种情况,因此需要一些额外的看法来解决这个问题。复制你的工作没有任何意义。 - Joachim Sauer
1
@GaborSch 根本问题在于我们希望实现一种高效的“在线更新”应用程序,用户只需从网站获取修改后的JAR文件。我可以创建与输入相同类文件的相同JAR文件。但问题是,当从相同的源文件编译时,类文件是否总是相同的。我们整个概念的成功与失败取决于这个事实。 - mstrap
2
@mstrap:终究是XY问题。你可以研究一下jar包的差分更新(这样即使只有一个字节的差异也不会导致整个jar包重新下载),并且你应该为你的发布提供明确的版本号,所以我认为这个问题已经没有意义了。 - Joachim Sauer
显示剩余11条评论

40

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


我将通过文件排序展示一个实际示例。

假设我们有两个 jar 文件: my1.jarMy2.jar。它们被放置在 lib 目录中,且并排放置。编译器按字母顺序读取它们(因为这是 lib),但如果文件系统不区分大小写,则顺序为 my1.jarMy2.jar;如果文件系统区分大小写,则顺序为 My2.jarmy1.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"; 
A.a(s); 

它将在不同情况下编译具有不同签名的方法调用。因此,根据您的文件系统区分大小写设置,您将得到不同的类作为结果。


2
@GaborSch 我对于同一JDK的字节码是否相同很感兴趣,也就是说,是否使用相同的javac。我会表述得更清楚。 - mstrap
2
@mstrap,我理解了你的问题,但答案仍然是:取决于供应商。javac不同,因为每个平台上都有不同的二进制文件(例如Win7、Linux、Solaris、Mac)。对于供应商来说,没有必要有不同的实现,但任何特定于平台的问题都可能影响结果(例如目录中的文件排序(考虑您的lib目录),字节序等)。 - gaborsch
1
通常,大多数javac都是用Java实现的(而javac只是一个简单的本地启动器),因此大多数平台差异不应该有影响。 - Joachim Sauer
2
@mstrap - 他的观点是,没有任何厂商有必要使其编译器在各个平台上产生完全相同的字节码,只需要产生相同结果即可。鉴于没有标准/规范/要求,对你的问题的答案是:“这取决于具体的供应商、编译器和平台”。 - Brian Roach
2
@mstrap,你问了一个理论问题,所以从理论上讲答案是“不会”。但是在实践中,大多数供应商都会生成相同的代码,但并不能保证。你愿意押多少赌注,以确保JDK8的第一个版本在所有平台上生成相同的字节码? - gaborsch
显示剩余9条评论

6

简短回答 - 不是


长回答

字节码在不同平台上可以不相同。这是JRE(Java Runtime Environment)知道如何执行字节码的方法。

如果您阅读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,我们将其类文件格式的版本表示为M.m。因此,类文件格式版本可以按字典顺序排序,例如,1.5<2.0<2.1。仅当v位于某个连续范围Mi.0 v Mj.m时,Java虚拟机实现才能支持版本为v的类文件格式。只有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范围内的类文件格式。

因此,调查所有这些显示,在不同平台上生成的类文件不需要完全相同。


请问你能否提供一个更详细的链接? - mstrap
我认为他们所说的“平台”是指Java平台,而不是操作系统。当然,在指示javac 1.7创建与1.6兼容的类文件时,会有所不同。 - mstrap
@mtk +1 用于显示编译期间为单个类生成了多少属性。 - gaborsch

3
首先,规范中绝对没有这样的保证。符合规范的编译器可以将编译时间作为附加(自定义)属性打印到生成的类文件中,而类文件仍然是正确的。但是,它会在每次构建时产生一个不同的字节级别的文件,而且容易如此。
其次,即使没有这样的恶心的技巧,除非两种情况下它的配置和输入都相同,否则没有理由期望编译器连续两次执行完全相同的操作。规范确实将源文件名描述为标准属性之一,添加空行到源文件可能会改变行号表。
第三,我从未遇到过由于主机平台而导致的构建差异(除了归因于classpath中差异的差异)。基于平台的代码(即本地代码库)不是类文件的一部分,而从字节码生成本地代码的实际过程发生在加载类之后。
第四(也是最重要的),想要知道这一点让人感到不好的流程味道(像代码气味一样,但是针对你对代码的行动方式)。如果可能,请对源进行版本控制,而不是对构建进行版本控制。如果确实需要对构建进行版本控制,则应该在整个组件级别上进行版本控制,而不是在单个类文件上进行版本控制。最好使用CI服务器(如Jenkins)来管理将源代码转换为可运行代码的过程。

2

我相信,如果您使用相同的JDK,生成的字节码将始终相同,与使用的硬件和操作系统无关。字节码的生成是由Java编译器完成的,它使用确定性算法将源代码转换为字节码。因此,输出始终相同。在这种情况下,只有对源代码进行更新才会影响输出结果。


4
你有相关的参考资料吗?正如我在评论中所说,这绝对不是C#的情况。因此,我很想看到一份声明Java是这种情况的参考资料。我特别认为多线程编译器可能会在不同运行中分配不同的标识符名称。 - RB.
1
这是我问题的答案和预期的结果,然而我同意 RB 的观点,有一个参考文献会很重要。 - mstrap
我认同这个观点。我不认为你会找到一个权威的参考资料。如果这对你很重要,那么你可以进行一项研究。收集一些主流的编译器,并在不同的平台上编译一些开源代码。比较字节文件。发布结果。记得在这里放置链接。 - emory

1
Java允许您在一个平台上编写/编译代码,然后在不同的平台上运行。据我所知,只有在生成于不同平台的类文件相同或技术上相同(即完全相同)时才可能实现这一点。
编辑
我所说的“技术上相同”的意思是,如果按字节比较它们,它们不需要完全相同。
因此,根据规范,在不同平台上的类的.class文件不需要逐字节匹配。

OP的问题是关于类文件是否相同或“技术上相同”的。 - bdesham
我想知道它们是否完全相同 - mstrap
是的,我的意思是如果逐字节比较它们可能不相同,这就是为什么我使用了“技术上相同”的词语。 - rai.skumar
@bdesham 他想知道它们是否相同。不确定你理解的“在技术上相同”是什么意思...这是你给出反对票的原因吗? - rai.skumar
@rai.skumar,您的回答基本上是说,“两个编译器总会产生行为相同的输出。” 当然这是正确的;这是Java平台的全部动机。 OP想知道生成的代码是否逐字节相同,而您在回答中没有涉及到这一点。 - bdesham
我认为我在我的第一条评论中已经解释了同样的事情...如果我的帖子没有明确说明,那是我的错。 - rai.skumar

1
总的来说,我必须说没有保证同一编译器在不同平台上编译相同源代码会产生相同的字节码。
我会研究涉及不同语言(代码页)的情况,例如支持日语的Windows。考虑多字节字符;除非编译器总是假定需要支持所有语言,否则它可能会优化为8位ASCII。 Java Language Specification中有一个关于二进制兼容性的章节。
在 SOM (Forman、Conner、Danforth 和 Raper 在 OOPSLA '95 会议上发表的文章中) 的发布到发布二进制兼容性框架内,Java 编程语言二进制文件在作者确定的所有相关转换下是二进制兼容的(关于实例变量的添加有一些注意事项)。使用他们的方案,以下是 Java 编程语言支持的一些重要的二进制兼容更改列表:
•重新实现现有方法、构造函数和初始化程序以提高性能。
•更改方法或构造函数,使其在先前抛出通常不会发生异常、进入无限循环或导致死锁的输入上返回值。
•向现有类或接口添加新字段、方法或构造函数。
•删除类的私有字段、方法或构造函数。
•当整个包更新时,删除包内类和接口的默认(仅限包)访问字段、方法或构造函数。
•重新排序现有类型声明中的字段、方法或构造函数。
•将方法向上移动到类层次结构中。
•重新排序类或接口直接超级接口列表。
•在类型层次结构中插入新的类或接口类型。
本章规定了所有实现所保证的二进制兼容性的最低标准。Java 编程语言保证当未知是否来自兼容源的类和接口的二进制文件混合使用时,它们是兼容的,但其源已按照这里描述的兼容方式进行了修改。请注意,我们讨论的是应用程序版本之间的兼容性。Java SE 平台版本之间的兼容性讨论超出了本章的范围。

该文章讨论了在更改Java版本时可能发生的情况。OP的问题是,如果我们在相同的Java版本内更改平台,会发生什么情况。否则,这是一个很好的发现。 - gaborsch
1
这是我能找到的最接近的答案。在语言规范和JVM规范之间存在一个奇怪的漏洞。到目前为止,我只能回答OP说“不能保证同一Java编译器在不同平台上运行时会产生相同的字节码”。 - Kelly S. French

1

针对这个问题:

"在哪些情况下,相同的javac可执行文件在不同的平台上运行时会生成不同的字节码?"

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

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


0

很可能是肯定的,但要得到精确的答案,需要在编译过程中搜索一些密钥或GUID生成。

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

P.S. JNI也很重要。

P.P.S. 我发现javac本身是用Java编写的。这意味着它在不同平台上是相同的。因此,它不会没有原因地生成不同的代码。所以,它只能通过本机调用来实现。


请注意,Java并不能完全屏蔽平台差异。列出目录内容时返回文件的顺序未定义,这可能会对编译器产生一些影响。 - Joachim Sauer

0

我会用另一种方式表达。

首先,我认为问题不在于确定性:

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

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

确保这一点的好方法是将 .class(或我的情况下的 .pyc)留在你的 git 阶段。你会发现,在团队的不同计算机之间,git 注意到 .pyc 文件之间的变化,即使 .py 文件没有进行任何更改(.pyc 仍然被重新编译)。

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


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