I would like to know why the .o file that we get from compiling a .c file that prints "Hello, World!" is larger than a Java .class file that also prints "Hello, World!"?
Why are compiled Java class files smaller than C compiled files?
4.6k views Asked by AHS AtThere are 9 answers
A class file is Java byte code .
It is most likely smaller since C/C++ libraries and operating system libraries are linked to the object code the C++ compiler produces to finally make an executable binary.
Simply put, it is like comparing Java byte code to object code produced by a C compiler before it is linked to create a binary. The difference is the fact that a JVM interprets the Java byte code to properly do what the program is meant to do whereas C requires information from the operating system since the operating system functions as the interpreter.
Also in C Every symbol (functions etc.) you reference from an external library at least once in one of the object files is imported. If you're using it in multiple object files, it's still imported just once. There are two ways this "importing" can happen. With static linking, the actual code for a function is copied into the executable. This increases file size but has the advantage that no external libraries (.dll/.so files) are needed. With dynamic linking this doesn't happen, but as a result your program requires additional libraries to run.
In Java, everything is "linked" dynamically, so to speak.
Java is compiled into a machine independent language. This means that after it is compiled it is then translated at run-time by the Java Virtual Machine (JVM). C is compiled to machine instructions and is therefore all of the binary for the program to run on the target machine.
Because Java is compiled to a machine independent language, the specific details for a particular machine are handled by the JVM. (i.e. C has machine specific overhead)
That is how I think about it anyway :-)
C programs, even though they're compiled to native machine code that runs on your processor (dispatched through the OS, of course), tend to need to do a lot of set up and tearing down for the operating system, loading dynamically-linked libraries like the C library, etc.
Java, on the other hand, compiles to bytecode for a virtual platform (basically a simulated computer-within-a-computer), which is specifically designed alongside Java itself, so a lot of this overhead (if it would even be necessary since both the code and the VM interface is well-defined) can be moved into the VM itself, leaving the program code to be lean.
It varies from compiler-to-compiler, though, and there are several options to reduce it or build code differently, which will have different effects.
All this said, it's not really that important.
In short: Java programs are compiled to Java byte code, which requires a separate interpreter (Java Virtual Machine) to be executed.
There is not a 100% guarantee that the .o file produced by the c-compiler is smaller, than the .class file produced by the Java compiler. It all depends of the implementation of the compiler.
A few potential reasons:
- The Java class file does not include initialization code at all. It just has your one class and one function in it - very small indeed. In comparison, the C program has some degree of statically-linked initialization code, and possibly DLL thunks.
- The C program may also have sections aligned to page boundaries - this would add a minimum of 4kb to the program size just like that, in order to ensure the code segment starts on a page boundary.
One of the key reasons for differences in the sizes of .o
and .class
files is that Java bytecodes are a bit higher-level than machine instructions. Not hugely higher-level, of course – it's still pretty low-level stuff – but that will make a difference because it effectively acts to compress the whole program. (Both C and Java code can have startup code in there.)
Another difference is that Java class files often represent relatively small pieces of functionality. While it is possible to have C object files that map to even smaller pieces, it's often more common to put more (related) functionality in a single file. The differences in scoping rules can also act to emphasize this (C doesn't really have anything that corresponds to module-level scope, but it does have file-level scope instead; Java's package scope works across multiple class files). You get a better metric if you compare the size of a whole program.
In terms of "linked" sizes, Java executable JAR files tend to be smaller (for a given level of functionality) because they're delivered compressed. It's relatively rare to deliver C programs in compressed form. (There's also differences in the size of the standard library, but they might as well be a wash because C programs can count on libraries other than libc being present, and Java programs have access to a huge standard library. Picking apart who has the advantage is awkward.)
Then, there's also the question of debugging information. In particular, if you compile a C program with debugging on that does IO, you'll get lots of information about types in the standard library included, just because it's a bit too awkward to filter it out. The Java code will only have debugging information about the actual compiled code because it can count on relevant information being available in the object file. Does this change the actual size of the code? No. But it can have a big impact on the file sizes.
Overall, I'd guess that it's hard to compare the sizes of C and Java programs. Or rather, you can compare them and easily learn nothing much useful.
The main cause of difference in size in this case is difference in file formats. For such a small program format of the ELF (.o
) file introduces serious overhead in terms of space.
For example, my sample .o
file of the "Hello, world" program takes 864 bytes. It consists of (explored with readelf
command):
- 52 bytes of file header
- 440 bytes of section headers (40 bytes x 11 sections)
- 81 bytes of section names
- 160 bytes of symbol table
- 43 bytes of code
- 14 bytes of data (
Hello, world\n\0
) - etc
.class
file of the similar program takes only 415 bytes, despite the fact that it contains more symbol names and these names are long. It consists of (explored with Java Class Viewer):
- 289 bytes of constant pool (includes constants, symbol names, etc)
- 94 bytes of method table (code)
- 8 bytes of attribute table (source file name reference)
- 24 bytes of fixed-size headers
See also:
Most (as much as 90% for simple functions) of an ELF-format .o
file is junk. For a .o
file containing a single empty function body, you can expect a size breakdown like:
- 1% code
- 9% symbol and relocation table (essential for linking)
- 90% header overhead, useless version/vendor notes stored by the compiler and/or assembler, etc.
If you want to see the real size of compiled C code, use the size
command.
Java uses Bytecode to be platform independent and "precompiled", but bytecode is used by interpreter and is served to be compact enough, so it is not the same that machine code which you can see in compiled C program. Just take a look at the full process of Java compilation:
this is the chain for Java Program to Machine code transformation. As you see bytecode is far away from machine code. I can't find in the Internet good stuff to show you this road on the real program (an example), everything I've found is this presentation, here you can see how each steps changes code presentation. I hope it answers you how and why compiled c program and Java bytecode are different.
UPDATE: All steps which are after "bytecode" are done by JVM in runtime depending on its decision to compile that code (that's another story... JVM is balancing between bytecode interpretation and its compiling to native platform dependent code)
Finally found good example, taken from Linear Scan Register Allocation for the Java HotSpot™ Client Compiler (btw good reading to understand what is going on inside JVM). Imagine that we have java program:
then its bytecode is:
each command takes 1 byte (JVM supports 256 commands, but in fact has less than that number) + arguments. Together it takes 27 bytes. I omit all stages, and here is ready to execute machine code:
it takes 83 (52 in hex + 1 byte) bytes in result.
PS. I don't take into account linking (was mentioned by others), as well as compiledc and bytecode file headers (probably they are different too; I don't know how is it with c, but in bytecode file all strings are moved to special header pool, and in program there is used its "position" in header etc.)
UPDATE2: Probably worth to mention, that java works with stack (istore/iload commands), though machine code based on x86 and most other platform works with registers. As you can see machine code is "full" of registers and that gives extra size to the compiled program in comparing with more simple stack-based bytecode.