【问题标题】:Why are compiled Java class files smaller than C compiled files?为什么编译的 Java 类文件比 C 编译的文件小?
【发布时间】:2011-06-17 19:35:35
【问题描述】:

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

【问题讨论】:

  • 我不知道(我有一些猜测,但我会把它们留给自己,让专家回答),但这是一个用于比较编译器输出大小的非常糟糕的程序。
  • 静态链接 C 程序并将其大小与 Java 程序 + Java VM 进行比较可能会更公平。
  • 另外,我们所说的要大多少?

标签: java c bytecode executable-format


【解决方案1】:

类文件是 Java 字节码。

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

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

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

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

【讨论】:

  • .o 文件不是完整的可执行文件,并且未与任何库链接。链接稍后进行。
【解决方案2】:

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

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

反正我就是这么想的:-)

【讨论】:

    【解决方案3】:

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

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

    不过,它因编译器而异,并且有几个选项可以减少它或以不同的方式构建代码,这将产生不同的效果。

    说了这么多,其实没那么重要。

    【讨论】:

    • 大部分设置和拆卸都在标准库函数中(例如_start_exit),因此在链接之前不包含在目标文件中。
    • 没错,我不久前通过指令跟踪了它的汇编指令,并回忆起那里。谢谢。尽管如此,与使用 nasm 或其他东西编译相比,仍然存在相当多的额外(必要)垃圾。
    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

      几个潜在的原因:

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

      【讨论】:

        【解决方案6】:

        .o.class 文件大小不同的一个关键原因是 Java 字节码比机器指令高级一点。当然,不是更高级别的——它仍然是相当低级的东西——但这会有所作为,因为它有效地压缩了整个程序。 (C 和 Java 代码都可以有启动代码。)

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

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

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

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

        【讨论】:

          【解决方案7】:

          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 程序到机器代码转换的链。如您所见,字节码与机器码相去甚远。我在网上找不到好东西给你展示这条路的真实程序(一个例子),我找到的都是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。我没有考虑链接(其他人提到过),以及compiledc和字节码文件头(可能它们也不同;我不知道c如何,但在字节码文件中所有字符串都被移动到特殊的头池,在程序中使用它在头等中的“位置”。)

          UPDATE2: 值得一提的是,java 使用堆栈(istore/iload 命令),尽管基于 x86 和大多数其他平台的机器代码使用寄存器。如您所见,机器代码“充满”了寄存器,与更简单的基于堆栈的字节码相比,这为编译的程序提供了额外的大小。

          【讨论】:

          • 感谢这个详尽的答案。
          • 你的答案看起来像一篇研究文章!!
          • 除了这远远不能解释全部差异。代码可能会缩短 2-3 倍,但差异通常要大得多。 @axtavt 的回答更准确地解释了差异
          【解决方案8】:

          在这种情况下,大小差异的主要原因是文件格式的差异。对于这样一个 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 字节的固定大小标头

          另请参阅:

          【讨论】:

            【解决方案9】:

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

            • 1% 代码
            • 9% 符号和重定位表(链接必不可少)
            • 90% 的标头开销、编译器和/或汇编器存储的无用版本/供应商注释等。

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-08-30
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多