为什么编译的Java类文件小于C编译文件?

时间:2011-01-29 17:15:07

标签: java c bytecode executable-format

我想知道为什么我们通过编译打印“Hello,World!”的.c文件获得的.o文件。大于Java .class文件,也打印“Hello,World!”?

9 个答案:

答案 0 :(得分:13)

Java使用Bytecode独立于平台并且“预编译”,但字节码由解释器使用并且足够紧凑,因此它与您在编译的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程序到机器代码转换的链。如您所见,字节码远离机器代码。我在互联网上找不到好东西向你展示真实程序的这条道路(一个例子),我发现的一切都是this presentation,在这里你可以看到每个步骤如何改变代码的呈现方式。我希望它能回答你编译的c程序和Java字节码的不同之处和原因。

<强>更新 “字节码”之后的所有步骤都是由JVM在运行时完成的,这取决于它编译该代码的决定(这是另一个故事...... JVM在字节码解释和编译为本机平台相关代码之间取得平衡)

最后找到了一个很好的例子,取自Linear Scan Register Allocation for the Java HotSpot™ Client Compiler(顺便说一句好读,以了解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。我没有考虑链接(被别人提到),以及compilec和字节码文件头(可能它们也不同;我不知道它是如何与c,但在字节码文件中所有字符串都移动到特殊的标题池,在程序中,它在标题等中使用了它的“位置”。)

UPDATE2:可能值得一提的是,java使用stack(istore / iload命令),虽然基于x86的机器代码和大多数其他平台都可以使用寄存器。正如您所看到的,机器代码是“完整”的寄存器,与更简单的基于堆栈的字节码相比,它为编译的程序提供了额外的大小。

答案 1 :(得分:7)

在这种情况下,尺寸差异的主要原因是文件格式的差异。对于这种ELF(.o)文件的小程序格式,在空间方面引入了严重的开销。

例如,我的“Hello,world”程序的样本.o文件采用 864字节。它由(用readelf命令探索)组成:

  • 52个字节的文件头
  • 440字节的节标题(40字节x 11节)
  • 81个字节的节名
  • 160字节的符号表
  • 43字节代码
  • 14个字节的数据Hello, world\n\0
类似程序的

.class文件仅占用 415字节,尽管它包含更多符号名称并且这些名称很长。它包括(用Java Class Viewer探讨):

  • 289个字节的常量池(包括常量,符号名称等)
  • 94字节的方法表(代码)
  • 8个字节的属性表(源文件名引用)
  • 24字节的固定大小标头

另见:

答案 2 :(得分:3)

C程序,即使它们被编译为在您的处理器上运行的本机机器代码(当然也是通过操作系统发送),往往需要对操作系统进行大量设置和拆卸,加载动态链接库,如C库等

另一方面,Java编译为虚拟平台的字节码(基本上是计算机内的模拟计算机),它与Java本身一起专门设计,所以很多开销(如果它甚至可能是必要的,因为代码和VM接口都是明确定义的)可以移动到VM本身,使程序代码保持精简。

它因编译器而异,但有几种方法可以减少它或以不同方式构建代码,这会产生不同的效果。

所有这些都说,它并不那么重要。

答案 3 :(得分:1)

简而言之:Java程序被编译为Java字节代码,这需要执行单独的解释器(Java虚拟机)。

没有100%保证c编译器生成的.o文件比Java编译器生成的.class文件小。这完全取决于编译器的实现。

答案 4 :(得分:1)

.o.class文件大小不同的一个主要原因是Java字节码比机器指令高一点。当然不是更高级别 - 它仍然是相当低级别的东西 - 但这会产生影响,因为它有效地用于压缩整个程序。 (C代码和Java代码都可以在其中包含启动代码。)

另一个区别是Java类文件通常代表相对较小的功能。虽然可以将C对象文件映射到更小的片段,但在单个文件中放置更多(相关)功能通常更为常见。范围规则的差异也可以强调这一点(C实际上没有任何与模块级范围相对应的内容,但它确实具有文件级范围; Java的包范围适用于多个类文件)。如果比较整个程序的大小,就会得到更好的指标。

就“链接”大小而言,Java可执行JAR文件往往更小(对于给定的功能级别),因为它们是经过压缩的。以压缩形式提供C程序相对较少。 (标准库的大小也存在差异,但它们也可能是一种清洗,因为C程序可以依赖libc以外的库,而Java程序可以访问一个巨大的标准库。挑选具有优势的人很尴尬。)

然后,还有调试信息的问题。特别是,如果您编译一个带有调试功能的C程序,那么您将获得包含标准库中类型的大量信息,只是因为过滤它有点太尴尬了。 Java代码只有关于实际编译代码的调试信息,因为它可以依赖目标文件中可用的相关信息。这会改变代码的实际大小吗?不会。但它会对文件大小产生很大影响。

总的来说,我猜想很难比较C和Java程序的大小。或者更确切地说,您可以比较它们并轻松学习任何有用的东西。

答案 5 :(得分:1)

ELF格式.o文件的大多数(简单函数多达90%)都是垃圾。对于包含单个空函数体的.o文件,您可以预期大小如下:

  • 1%代码
  • 9%符号和重定位表(对于链接至关重要)
  • 90%的头文件开销,编译器和/或汇编器存储的无用版本/供应商注释等。

如果要查看已编译C代码的实际大小,请使用size命令。

答案 6 :(得分:0)

类文件是Java字节码。

由于C / C ++库和操作系统库链接到C ++编译器生成的目标代码以最终生成可执行二进制文件,因此它很可能更小。

简单地说,就像将Java字节代码与C编译器生成的目标代码进行比较,然后将其链接到创建二进制文件。不同之处在于JVM解释Java字节代码以正确执行程序要执行的操作,而C需要来自操作系统的信息,因为操作系统充当解释器。

同样在C中,每个符号(函数等)都会导入一个外部库,至少在其中一个目标文件中引用一次。如果您在多个目标文件中使用它,它仍然只导入一次。这种“导入”有两种方式可以实现。使用静态链接,函数的实际代码将复制到可执行文件中。这会增加文件大小,但其优点是不需要外部库(.dll / .so文件)。通过动态链接,这不会发生,但结果是您的程序需要运行其他库。

在Java中,一切都是动态地“链接”,可以这么说。

答案 7 :(得分:0)

Java被编译成与机器无关的语言。这意味着在编译之后,它将在运行时由Java虚拟机(JVM)进行转换。 C被编译为机器指令,因此是程序在目标机器上运行的所有二进制文件。

因为Java被编译为与机器无关的语言,所以特定机器的具体细节由JVM处理。 (即C具有机器特定的开销)

无论如何,这就是我的想法: - )

答案 8 :(得分:-1)

一些可能的原因:

  • Java类文件根本不包含初始化代码。它只有你的一个类和一个函数 - 非常小。相比之下,C程序有一定程度的静态链接初始化代码,可能还有DLL thunks。
  • C程序也可能具有与页面边界对齐的部分 - 这将为程序大小添加至少4kb,以确保代码段在页面边界上开始。