我想知道为什么编译一个打印“Hello, World!”的.c文件生成的.o文件比打印同样内容的Java .class文件要大?
我想知道为什么编译一个打印“Hello, World!”的.c文件生成的.o文件比打印同样内容的Java .class文件要大?
Java使用字节码实现跨平台和“预编译”,但是字节码由解释器使用,并且被设计为足够紧凑,因此与编译的C程序中可以看到的机器码不同。只需查看Java编译的完整过程:
Java program
-> Bytecode
-> High-level Intermediate Representation (HIR)
-> Middle-level Intermediate Representation (MIR)
-> Low-level Intermediate Representation (LIR)
-> Register allocation
-> EMIT (Machine Code)
这是Java程序转换为机器代码的过程。如您所见,字节码与机器代码相差甚远。我在互联网上找不到好的资料来演示这一过程,即真实程序的示例;我找到的所有内容都在这个演示中,您可以看到每个步骤如何改变代码的呈现方式。希望它能回答您编译的C程序和Java字节码之间的差异是如何产生的。
更新: 在“字节码”之后的所有步骤都是由JVM在运行时执行的,这取决于其决定是否编译该代码(这是另一个故事……JVM在字节码解释和编译成本地平台相关代码之间进行平衡)
最终找到了很好的例子,取自Java HotSpot™客户端编译器的线性扫描寄存器分配(顺便说一下,这是理解JVM内部发生了什么的好读物)。想象一下我们有一个Java程序:
public static void fibonacci() {
int lo = 0;
int hi = 1;
while (hi < 10000) {
hi = hi + lo;
lo = hi - lo;
print(lo);
}
}
那么它的字节码是:
0: iconst_0
1: istore_0 // lo = 0
2: iconst_1
3: istore_1 // hi = 1
4: iload_1
5: sipush 10000
8: if_icmpge 26 // while (hi < 10000)
11: iload_1
12: iload_0
13: iadd
14: istore_1 // hi = hi + lo
15: iload_1
16: iload_0
17: isub
18: istore_0 // lo = hi - lo
19: iload_0
20: invokestatic #12 // print(lo)
23: goto 4 // end of while-loop
26: return
每个命令占用1个字节(JVM支持256个命令,但实际上没有那么多)+参数。总共需要27个字节。我省略了所有阶段,下面是可执行的机器代码:
00000000: mov dword ptr [esp-3000h], eax
00000007: push ebp
00000008: mov ebp, esp
0000000a: sub esp, 18h
0000000d: mov esi, 1h
00000012: mov edi, 0h
00000017: nop
00000018: cmp esi, 2710h
0000001e: jge 00000049
00000024: add esi, edi
00000026: mov ebx, esi
00000028: sub ebx, edi
0000002a: mov dword ptr [esp], ebx
0000002d: mov dword ptr [ebp-8h], ebx
00000030: mov dword ptr [ebp-4h], esi
00000033: call 00a50d40
00000038: mov esi, dword ptr [ebp-4h]
0000003b: mov edi, dword ptr [ebp-8h]
0000003e: test dword ptr [370000h], eax
00000044: jmp 00000018
00000049: mov esp, ebp
0000004b: pop ebp
0000004c: test dword ptr [370000h], eax
00000052: ret
结果占用了83个字节(52个十六进制+1个字节)。
PS. 我没有考虑链接(其他人已经提到了),以及编译和字节码文件头(它们可能也不同;我不知道C语言是怎么样的,但在字节码文件中,所有字符串都被移动到特殊的头池中,在程序中使用其在头部的“位置”等)。
UPDATE2:值得一提的是,Java使用堆栈(istore/iload命令),而基于x86和大多数其他平台的机器代码则使用寄存器。如您所见,机器代码“充满”寄存器,这使得编译程序与更简单的基于栈的字节码相比增加了额外的大小。
.o
)文件格式会引入严重的空间开销。.o
文件需要864字节。它由以下内容组成(使用 readelf
命令探索):Hello, world \ n \ 0
)
- 等等.class
文件只需415字节,尽管它包含更多的符号名称且这些名称很长。它由以下内容组成(使用Java Class Viewer探索):_start
和 _exit
),因此在链接之前不会包含在对象文件中。 - user395760一个类文件是Java字节码。
它很可能更小,因为C/C++库和操作系统库链接到C++编译器生成的目标代码中,最终生成可执行二进制文件。
简单来说,这就像将Java字节码与C编译器在链接创建二进制文件之前生成的目标代码进行比较。区别在于JVM解释Java字节码以正确执行程序所需的操作,而C需要来自操作系统的信息,因为操作系统充当解释器。
而且,在C中,您从外部库引用的每个符号(函数等)至少在一个对象文件中引用一次。如果您在多个对象文件中使用它,则仍然只会被导入一次。这种“导入”可以通过两种方式实现。使用静态链接,函数的实际代码将复制到可执行文件中。这会增加文件大小,但优点是不需要外部库(.dll/.so文件)。而动态链接则不会发生这种情况,但结果是您的程序需要其他库才能运行。
在Java中,所有内容都是“动态”链接的。
.o
文件不是一个完整的可执行文件,也没有与任何库链接。链接操作将在之后进行。 - user395760.o
和.class
文件大小差异的关键原因之一是Java字节码比机器指令高级一些。当然,它仍然是相当低级的东西,但这会产生影响,因为它有效地压缩了整个程序。(C和Java代码都可以在其中有启动代码。)
另一个区别是Java类文件通常表示相对较小的功能块。虽然可能存在映射到更小块的C对象文件,但通常更常见的是将更多(相关)功能放入单个文件中。作用域规则的差异也可以强调这一点(C实际上没有任何对应于模块级别作用域的东西,但它确实有文件级别作用域;Java的包级作用域适用于多个类文件)。如果比较整个程序的大小,则可以得到更好的度量。
就“链接”大小而言,Java 可执行 JAR 文件通常比较小(在给定的功能级别下),因为它们是经过压缩传输的。相对来说,C 程序很少以压缩形式传输。(标准库大小也有所不同,但因为 C 程序可以指望除 libc 之外的其它库存在,而且 Java 程序可以访问巨大的标准库,所以这两者可以算作持平。挑出优劣显得有些棘手。)大多数(高达90%的简单函数)ELF格式.o
文件是垃圾代码。对于一个包含单个空函数体的.o
文件,您可以期望如下大小分解:
如果您想要查看已编译的C代码的真实大小,请使用size
命令。
Java被编译成一种与机器无关的语言。这意味着在编译后,Java虚拟机(JVM)会在运行时将其翻译成机器可读的指令。C被编译为机器指令,因此所有二进制代码都是为了在目标机器上运行程序。
由于Java被编译为一种与机器无关的语言,特定机器的具体细节由JVM处理。(即C具有机器特定的开销)
这就是我对它的理解方式 :-)
可能有几个原因: