Java类文件的创建是否确定性的?


94

当使用相同的JDK(即相同的javac可执行文件)时,生成的类文件是否始终相同?取决于操作系统硬件,会有所区别吗?除JDK版本外,是否还有其他因素导致差异?是否有任何编译器选项来避免差异?是仅在理论上可能有所不同,还是Oracle javac实际上针对相同的输入和编译器选项生成了不同的类文件?

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

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

Update 3在Oracle编译器中的理论差异和实践差异之间的区别。

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


5
@Gamb CORA并没有意味着,如果在不同的平台上编译字节码将是完全一样的; 这意味着生成的字节码将执行完全相同的操作。
dasblinkenlight

10
你为什么在乎?这闻起来像XY问题
Joachim Sauer 2013年

4
@JoachimSauer考虑是否对二进制文件进行版本控制-您可能只想在源代码已更改的情况下检测更改,但是如果JDK可以任意更改输出二进制文件,您会知道这不是一个明智的主意。
RB。

7
@RB .:允许编译器产生任何表示已编译代码的符合字节代码。实际上,某些编译器更新程序修复了会产生略有不同的代码的错误(通常具有相同的运行时行为)。换句话说:如果要检测源更改,请检查源更改。
Joachim Sauer

3
@dasblinkenlight:您假设他们声称拥有的答案实际上是正确的并且是最新的(令人怀疑,鉴于问题来自2003年)。
Joachim Sauer

Answers:


68

让我们这样说:

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

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

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

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

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

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

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


@GaborSch它缺少了什么?“相同的javac可执行文件在不同的平台上运行时,在什么情况下会产生不同的字节码?” 基本上取决于产生编译器的小组的想法
-emory

3
好吧,对我而言,这是足够的理由,使其不必依赖它:如果我依赖于编译器始终生成相同代码这一事实,则更新的JDK可能会破坏我的构建/归档系统。
Joachim Sauer

3
@GaborSch:您已经有一个很好的例子,可以解决这个问题。复制工作没有任何意义。
约阿希姆·绍尔

1
@GaborSch根本问题是我们想对我们的应用程序执行有效的“在线更新”,对于该应用程序,用户只能从网站上获取经过修改的JAR。我可以创建具有相同类文件作为输入的相同JAR。但是问题是,从相同的源文件编译时,类文件是否总是相同的。我们的整个概念因这个事实而站不住脚。
mstrap

2
@mstrap:毕竟这是一个XY问题。好吧,您可以研究一下jar的差异更新(因此,即使是一个字节的差异也不会导致整个jar的重新下载),并且无论如何,您都应该为发行版提供明确的版本号,因此我认为整个观点尚无定论。
Joachim Sauer

38

编译器没有义务在每个平台上产生相同的字节码。您应咨询其他供应商的javac实用程序以获取特定答案。


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

假设我们有2个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); 

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


1
+1 Eclipse编译器和javac之间存在无数差异,例如,如何生成综合构造函数
Paul Bellora

2
@GaborSch我对相同的JDK(即相同​​的javac)的字节码是否相同感兴趣。我会说得更清楚。
mstrap

2
@mstrap我理解您的问题,但答案仍然相同:取决于供应商。两者javac不一样,因为每个平台(例如Win7,Linux,Solaris,Mac)上的二进制文件都不相同。对于供应商而言,采用不同的实现没有任何意义,但是任何特定于平台的问题都可能影响结果(例如,目录中的文件排序(在目录中考虑lib),字节序等)。
gaborsch

1
通常,大多数javac都是用Java实现的(并且javac只是一个简单的本机启动器),因此大多数平台差异都不会产生影响。
约阿希姆·绍尔

2
@mstrap -他提出的观点是,有没有要求对任何供应商,使他们的编译器产生完全相同跨平台的字节码相同,只是所产生的字节码产生相同的结果。鉴于没有标准/规范/要求,您的问题的答案是“这取决于特定的供应商,编译器和平台”。
Brian Roach

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

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(含)范围内的版本的类文件格式。

因此,研究所有这些结果表明,在不同平台上生成的类文件不必相同。


您能否提供更详细的链接?
mstrap

我认为“平台”是指Java平台,而不是操作系统。当然,当指示javac 1.7创建与1.6兼容的类文件时,会有区别。
mstrap

@mtk +1显示在编译期间为单个类生成了多少个属性。
gaborsch

3

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

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

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

第四点(也是最重要的一点),它很想知道一个不好的过程气味(像代码气味,但是对于您如何执行代码)。如果可能,请对源进行版本化,而不是对版本进行版本化;如果确实需要对版本进行版本化,请在整个组件级别而不是在单个类文件上进行版本化。优先使用CI服务器(例如Jenkins)来管理将源代码转换为可运行代码的过程。


2

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


3
您对此有参考吗?正如我在问题注释中所说的,对于C#绝对不是这种情况,因此很乐意看到一个引用说明它对于Java是这种情况。我特别认为多线程编译器可能会在不同的运行中分配不同的标识符名称。
RB。

1
这是我的问题的答案以及我的期望,但是我同意RB的建议,这一点很重要。
mstrap

我相信一样。我认为您不会找到明确的参考。如果对您很重要,那么您可以进行研究。收集一堆领先者,并在不同的平台上尝试它们,编译一些开源代码。比较字节文件。发布结果。确保在此处放置一个链接。
emory

1

总体而言,我不得不说不能保证当同一编译器在不同平台上编译时,同一源将产生相同的字节码。

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

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

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

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

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

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

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

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

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

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

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

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

本章为所有实现指定了二进制兼容性的最低标准。当混合类和接口的二进制文件时,Java编程语言可以保证兼容性,这些类和接口的二进制文件不属于可兼容的来源,但是其来源已按照此处描述的兼容方式进行了修改。请注意,我们正在讨论应用程序版本之间的兼容性。关于Java SE平台发行版之间兼容性的讨论不在本章范围之内。


该文章讨论了在更改Java版本时可能发生的情况。OP的问题是,如果我们在同一Java版本中更改平台,会发生什么情况。否则,这是一个很好的收获。
gaborsch

1
尽我所能。在语言规范和JVM规范之间有一个奇怪的漏洞。到目前为止,我必须用“不能保证同一Java编译器在不同平台上运行时会产生相同字节码”来回答OP。
凯利·法国

1

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

编辑

技术上讲,我的意思是:如果您逐字节进行比较,则它们不必完全相同。

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


OP的问题类文件是相同还是“技术上相同”。
bdesham

我对它们是否相同很感兴趣。
mstrap

回答是。我的意思是,如果您逐字节比较它们,它们可能并不相同,这就是为什么我在技术上使用了相同的词。
rai.skumar

@bdesham他想知道它们是否相同。不知道您对“技术上相同”的理解...是拒绝投票的原因吗?
rai.skumar

@ rai.skumar您的回答基本上是:“两个编译器将始终产生行为相同的输出。” 当然是这样;这是Java平台的全部动机。OP想要知道发出的代码是否字节对字节相同,而您在回答中没有解决这个问题。
bdesham

1

对于这个问题:

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

交叉编译例子展示了如何使用javac选项:-target版本

该标志生成与我们在调用此命令时指定的Java版本兼容的类文件。因此,根据我们在使用此选项的补偿过程中提供的属性,类文件将有所不同。


0

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

我不记得发生这种情况的情况。例如,具有用于序列化目的的ID,则对其进行硬编码,即由程序员或IDE生成。

PS JNI也很重要。

我发现PPS javac本身是用Java编写的。这意味着在不同平台上它是相同的。因此,它不会无缘无故生成不同的代码。因此,它只能通过本地调用来做到这一点。


请注意,Java并不能使您免受所有平台差异的影响。未列出列出目录内容时返回的文件顺序,这可能会对编译器产生一些影响。
Joachim Sauer

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控制,但只能确定类文件是否已更改,而不能首先确定源文件是否已更改。


0

我会换种说法。

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

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

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

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

至少那是我观察到的。因此,将* .pyc和* .class放在您的.gitignore中!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.