95

当使用相同的JDK(即相同​​的javac可执行文件)时,生成的类文件是否总是相同的?是否会因操作系统硬件而有所不同?除了JDK版本之外,还有其他因素导致差异吗?是否有任何编译器选项可以避免差异?只是理论上可能存在差异,还是Oraclejavac实际上为相同的输入和编译器选项生成不同的类文件?

更新1我对生成感兴趣,即编译器输出,而不是类文件是否可以在各种平台上运行。

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

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

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

4

11 回答 11

69

让我们这样说:

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

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

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

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

更新:我最近偶然发现了这篇关于在 Java 7中实现on的有趣博客文章switchString。在这篇博文中,有一些相关的部分,我将在这里引用(强调我的):

为了使编译器的输出可预测和可重复,这些数据结构中使用的映射和集合是LinkedHashMaps 和LinkedHashSets 而不仅仅是HashMapsand HashSets。就给定编译期间生成的代码的功能正确性而言,使用HashMapHashSet会很好;迭代顺序无关紧要。然而,我们发现它javac的输出不因系统类的实现细节而变化是有益的。

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

于 2013-02-20T17:11:06.993 回答
39

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


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

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

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

于 2013-02-20T16:43:15.733 回答
6

简短的回答 -


长答案

对于不同的平台,它们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 项的值是这个类文件的次要和主要版本号。主要和次要版本号一起确定了类文件格式的版本。如果一个类文件的主版本号为 M 和次要版本号为 m,我们将其类文件格式的版本表示为 Mm。因此,类文件格式版本可以按字典顺序排序,例如 1.5 < 2.0 < 2.1。当且仅当 v 位于某个连续范围 Mi.0 v Mj.m 中时,Java 虚拟机实现才能支持版本 v 的类文件格式。只有 Sun 可以指定符合 Java 平台某个发布级别的 Java 虚拟机实现可以支持的版本范围。1

通过脚注阅读更多内容

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 范围内的版本的类文件格式(包括 45.0 到 46.0)。

因此,调查这一切表明在不同平台上生成的类文件不必相同。

于 2013-02-20T16:44:56.413 回答
3

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

其次,即使没有这些令人讨厌的技巧,也没有理由期望编译器连续两次执行完全相同的操作,除非它的配置和输入在两种情况下都相同。该规范确实将源文件名描述为标准属性之一,并且在源文件中添加空行可以很好地更改行号表。

第三,由于主机平台,我从未遇到过任何构建差异(除了由于类路径上的差异引起的差异)。因平台而异的代码(即本机代码库)不是类文件的一部分,从字节码实际生成本机代码是在加载类之后发生的。

第四(也是最重要的)想要知道这一点,它散发出一种糟糕的过程气味(就像代码气味,但对于您如何处理代码)。如果可能,对源代码进行版本控制,而不是对构建版本,如果您确实需要对构建版本进行版本控制,请在整个组件级别而不是在单个类文件上进行版本控制。根据偏好,使用 CI 服务器(例如 Jenkins)来管理将源代码转换为可运行代码的过程。

于 2013-02-20T22:51:57.713 回答
2

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

于 2013-02-20T16:45:02.803 回答
1

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

编辑

我所说的技术上相同的评论是这样的。如果逐字节比较,它们不需要完全相同。

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

于 2013-02-20T16:48:30.563 回答
1

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

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

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

在 SOM 中发布到发布二进制兼容性的框架内(Forman、Conner、Danforth 和 Raper,OOPSLA '95 论文集),Java 编程语言二进制文件在作者确定的所有相关转换下都是二进制兼容的(有一些警告关于添加实例变量)。使用他们的方案,这里列出了 Java 编程语言支持的一些重要的二进制兼容更改:

• 重新实现现有方法、构造函数和初始化程序以提高性能。

• 更改方法或构造函数以返回输入的值,这些输入先前要么抛出通常不应发生的异常,要么因进入无限循环或导致死锁而失败。

• 向现有类或接口添加新字段、方法或构造函数。

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

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

• 对现有类型声明中的字段、方法或构造函数重新排序。

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

• 重新排序类或接口的直接超接口列表。

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

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

于 2013-02-20T18:05:11.983 回答
1

对于这个问题:

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

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

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

于 2013-02-27T09:30:51.730 回答
0

最有可能的答案是“是”,但要获得准确的答案,确实需要在编译期间搜索一些键或 guid 生成。

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

PS JNI 也很重要。

PPS 我发现它javac本身是用 java 编写的。这意味着它在不同平台上是相同的。因此,它不会无缘无故地生成不同的代码。因此,它只能通过本机调用来执行此操作。

于 2013-02-20T16:43:32.483 回答
0

我会换一种说法。

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

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

其次,如果您将其重新表述为“相同源代码文件的字节码文件有多相似?”,那么,您不能依赖它们相似的事实

确保这一点的一个好方法是将 .class (或在我的情况下为 .pyc )留在你的 git 阶段。您会意识到,在您团队中的不同计算机中,git 会注意到 .pyc 文件之间的更改,而 .py 文件没有发生任何更改(并且 .pyc 无论如何都会重新编译)。

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

于 2013-02-27T10:10:59.680 回答
0

有两个问题。

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

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

即使当前存在的每个编译器在所有情况下(不同的硬件等)都生成相同的字节码,明天的答案可能会有所不同。如果您从不打算更新 javac 或您的操作系统,您可以在特定情况下测试该版本的行为,但如果您从 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?

那是不可知的。

我不知道配置管理是否是您提出问题的原因,但这是可以理解的关心原因。比较字节码是一种合法的 IT 控制,但仅确定类文件是否更改,而不是确定源文件是否更改。

于 2013-02-27T13:33:40.303 回答