更新1:我对生成的编译器输出感兴趣,而不是类文件是否可以在各种平台上运行。
更新2:通过“相同的JDK”,我也指的是相同的javac可执行文件。
更新3:区分Oracle编译器中理论差异和实际差异。
编辑,添加重新表述的问题: “在哪些情况下,相同的javac可执行文件在不同的平台上运行会产生不同的字节码?”
这么说吧:
我可以轻松地制作一个完全符合规范的Java编译器,其在给定相同的.java
文件时永远不会产生相同的.class
文件。
我可以通过调整所有类型的字节码构造或简单地向我的方法添加多余的属性(这是允许的)来实现这一点。
鉴于规范并没有要求编译器生成按字节完全相同的类文件,因此我会避免依赖这样的结果。
然而,我检查过几次,使用相同的编译器、相同的开关(以及相同的库!)编译相同的源文件确实会产生相同的.class
文件。
更新:我最近偶然发现了这篇有关Java 7中String
开关实现的有趣博客文章。 在这篇博客文章中,有一些相关部分,我将在此引用(重点是我的):
为了使编译器的输出可预测且可重复,这些数据结构中使用的映射和集合是
LinkedHashMap
和LinkedHashSet
,而不仅仅是HashMaps
和HashSets
。在给定编译期间生成的代码的功能正确性方面,使用HashMap
和HashSet
都可以; 迭代顺序无关紧要。 但是,我们发现具有javac
的输出不因系统类的实现细节而发生变化是有益的。
下面的内容非常清楚地说明了这个问题:只要编译器符合规范,它就不需要以确定性的方式运行。然而,编译器开发人员意识到,通常情况下尽量尝试(如果代价不太高的话)是一个好主意。
编译器没有义务在每个平台上产生相同的字节码。您应该查询不同供应商的 javac
实用程序以获得具体答案。
我将通过文件排序展示一个实际示例。
假设我们有两个 jar 文件: my1.jar
和 My2.jar
。它们被放置在 lib
目录中,且并排放置。编译器按字母顺序读取它们(因为这是 lib
),但如果文件系统不区分大小写,则顺序为 my1.jar
、My2.jar
;如果文件系统区分大小写,则顺序为 My2.jar
、my1.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);
它将在不同情况下编译具有不同签名的方法调用。因此,根据您的文件系统区分大小写设置,您将得到不同的类作为结果。
javac
不同,因为每个平台上都有不同的二进制文件(例如Win7、Linux、Solaris、Mac)。对于供应商来说,没有必要有不同的实现,但任何特定于平台的问题都可能影响结果(例如目录中的文件排序(考虑您的lib
目录),字节序等)。 - gaborschjavac
都是用Java实现的(而javac
只是一个简单的本地启动器),因此大多数平台差异不应该有影响。 - Joachim Sauer简短回答 - 不是
字节码
在不同平台上可以不相同。这是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范围内的类文件格式。
因此,调查所有这些显示,在不同平台上生成的类文件不需要完全相同。
我相信,如果您使用相同的JDK,生成的字节码将始终相同,与使用的硬件和操作系统无关。字节码的生成是由Java编译器完成的,它使用确定性算法将源代码转换为字节码。因此,输出始终相同。在这种情况下,只有对源代码进行更新才会影响输出结果。
针对这个问题:
"在哪些情况下,相同的javac可执行文件在不同的平台上运行时会生成不同的字节码?"
交叉编译示例展示了我们如何使用Javac选项:-target版本
此标志生成与我们在调用此命令时指定的Java版本兼容的类文件。因此,使用此选项进行编译时提供的属性将导致类文件不同。
很可能是肯定的,但要得到精确的答案,需要在编译过程中搜索一些密钥或GUID生成。
我记不起来发生这种情况的情形了。例如,为了序列化目的而有ID,它是硬编码的,即由程序员或IDE生成。
P.S. JNI也很重要。
P.P.S. 我发现javac
本身是用Java编写的。这意味着它在不同平台上是相同的。因此,它不会没有原因地生成不同的代码。所以,它只能通过本机调用来实现。
我会用另一种方式表达。
首先,我认为问题不在于确定性:
当然是确定性的:在计算机科学中很难实现随机性,并且编译器没有任何理由在这里引入它。
其次,如果你通过“相同源代码文件的字节码文件有多相似?”来重新表述它,那么不,你不能依赖它们会相似的事实。
确保这一点的好方法是将 .class(或我的情况下的 .pyc)留在你的 git 阶段。你会发现,在团队的不同计算机之间,git 注意到 .pyc 文件之间的变化,即使 .py 文件没有进行任何更改(.pyc 仍然被重新编译)。
至少这就是我观察到的。所以把 *.pyc 和 *.class 放在你的 .gitignore 中!