If one needs different JVMs for different architectures I can't figure out what is the logic behind introducing this concept. In other languages we need different compilers for different machines, but in Java we require different JVMs so what is the logic behind introducing the concept of a JVM or this extra step??
The logic is that JVM bytecode is a lot simpler than Java source code.
Compilers can be thought of, at a highly abstract level, as having three basic parts: parsing, semantic analysis, and code generation.
Parsing consists of reading the code and turning it into a tree representation inside the compiler's memory. Semantic analysis is the part where it analyzes this tree, figures out what it means, and simplifies all the high-level constructs down to lower-level ones. And code generation takes the simplified tree and writes it out into a flat output.
With a bytecode file, the parsing phase is greatly simplified, since it's written in the same flat byte stream format that the JIT uses, rather than a recursive (tree-structured) source language. Also, a lot of the heavy lifting of the semantic analysis has already been performed by the Java (or other language) compiler. So all it has to do is stream-read the code, do minimal parsing and minimal semantic analysis, and then perform code generation.
This makes the task the JIT has to perform a lot simpler, and therefore a lot faster to execute, while still preserving the high-level metadata and semantic information that makes it possible to theoretically write single-source, cross-platform code.
Intermediate representations of various sorts are increasingly common in compiler / runtime design, for a few reasons.
In Java's case, the number one reason initially was probably portability: Java was heavily marketed initially as "Write Once, Run Anywhere". While you can achieve this by distributing the source code and using different compilers to target different platforms, this has a few downsides:
- compilers are complex tools which have to understand all the convenience syntaxes of the language; bytecode can be a simpler language, since it is closer to machine-executable code than human-readable source; this means:
- compilation may be slow compared to executing bytecode
- compilers targeting different platforms may end up producing different behaviour, or not keeping up with language changes
- producing a compiler for a new platform is a lot harder than producing a VM (or bytecode-to-native compiler) for that platform
- distributing source code is not always desirable; bytecode offers some protection against reverse engineering (although it's still fairly easy to decompile unless deliberately obfuscated)
Other advantages of an intermediate representation include:
- optimisation, where patterns can be spotted in the bytecode and compiled down to faster equivalents, or even optimised for special cases as the program runs (using a "JIT", or "Just In Time", compiler)
- interoperability between multiple languages in the same VM; this has become popular with the JVM (e.g. Scala), and is the explicit aim of the .net framework
It sounds like you're wondering why we don't just distribute source code. Let me turn that question around: why don't we just distribute machine code?
Clearly the answer here is that Java, by design, does not assume it knows what the machine is where your code will run; it could be a desktop, a super-computer, a phone, or anything in between and beyond. Java leaves room for the local JVM compiler to do its thing. In addition to increasing the portability of your code, this has the nice benefit of allowing the compiler to do things like take advantage of machine-specific optimizations, if they exist, or still produce at least working code if they do not. Things like SSE instructions or hardware acceleration can be used on only the machines that support them.
Seen in this light, the reasoning for using byte-code over raw source code is clearer. Getting as close to raw machine language as possible allows us to realize or partially realize some of the benefits of machine code, such as:
- Faster startup times, since some of the compiling and analysis is already done.
- Security, since the byte-code format has a built-in mechanism for signing the distribution files (source could do this by convention, but the mechanism to accomplish this isn't built-in the way it is with byte code).
Note that I don't mention faster execution. Both source code and byte code are or can (in theory) be fully compiled to the same machine code for actual execution.
Additionally, byte code allows for some improvements over machine code. Of course there are the platform independence and hardware-specific optimizations I mentioned earlier, but there are also things like servicing the JVM compiler to produce new execution paths from old code. This can be to patch security issues, or if new optimizations are discovered, or to take advantage of new hardware instructions. In practice it's rare to see big changes this way, because it can expose bugs, but it is possible, and it's something that happens in small ways all the time.
There seem to be at least two different possible questions here. One is really about compilers in general, with Java basically just an example of the genre. The other is more specific to Java the specific byte codes it uses.
Compilers in general
Let's first consider the general question: why would a compiler use some an intermediate representation in the process of compiling source code to run on some particular processor?
One answer to that is fairly simple: it converts an O(N * M) problem into an O(N + M) problem.
If we're given N source languages, and M targets, and each compiler is completely independent, then we need N * M compilers to translate all those source languages to all those targets (where a "target" is something like a combination of a processor and OS).
If, however, all those compilers agree on a common intermediate representation, then we can have N compiler front ends that translate the source languages to the intermediate representation, and M compiler back ends that translate the intermediate representation to something suitable for a specific target.
Better still, it separates the problem into two more or less exclusive domains. People who know/care about language design, parsing and things like that can concentrate on compiler front ends, while people who know about instruction sets, processor design, and things like that can concentrate on the back end.
So, for example, given something like LLVM, we have lots of front ends for various different languages. We also have back-ends for lots of different processors. A language guy can write a new front-end for his language, and quickly support lots of targets. A processor guy can write a new back-end for his target without dealing with language design, parsing, etc.
Separating compilers into a front end and back end, with an intermediate representation to communicate between the two isn't original with Java. It's been pretty common practice for a long time (since well before Java came along, anyway).
To the extent that Java added anything new in this respect, it was in the distribution model. In particular, even though compilers have been separated into front-end and back-end pieces internally for a long time, they were typically distributed as a single product. For example, if you bought a Microsoft C compiler, internally it had a "C1" and a "C2", which were the front-end and back-end respectively--but what you bought was just "Microsoft C" that included both pieces (with a "compiler driver" that coordinated operations between the two). Even though the compiler was built in two pieces, to a normal developer using the compiler it was just a single thing that translated from source code to object code, with nothing visible in between.
Java, instead, distributed the front-end in the Java Development Kit, and the back-end in the Java Virtual Machine. Every Java user had a compiler back-end to target whatever system he was using. Java developers distributed code in the intermediate format, so when a user loaded it, the JVM did whatever was necessary to execute it on their particular machine.
Note that this distribution model wasn't entirely new either. Just for example, the UCSD P-system worked similarly: compiler front ends produced P-code, and each copy of the P-system included a virtual machine that did what was necessary to execute the P-code on that particular target1.
Java byte code is quite similar to P-code. It's basically instructions for a fairly simple machine. That machine is intended to be an abstraction of existing machines, so it's fairly easy to translate quickly to almost any specific target. Ease of translation was important early on because the original intent was to interpret byte codes, much like P-System had done (and, yes, that's exactly how the early implementations worked).
Java byte code is easy for a compiler front-end to produce. If (for example) you have a fairly typical tree representing an expression it's typically pretty easy to traverse the tree, and generate code fairly directly from what you find at each node.
Java byte codes are quite compact--in most cases, much more compact than either the source code or machine code for most typical processors (and, especially for most RISC processors, such as the SPARC that Sun sold when they designed Java). This was particularly important at the time, because one major intent of Java was to support applets--code embedded in web pages that would be downloaded before execution--at a time when most people accessed the we via modems over phone lines at around 28.8 kilobits per second (though, of course, there were still quite a few people using older, slower modems).
The major weakness of Java byte codes is that they aren't particularly expressive. Although they can express the concepts present in Java pretty well, they don't work nearly so well for expressing concepts that aren't part of Java. Likewise, while it's easy to execute byte codes on most machines, it's much harder to that in a way that takes full advantage of any particular machine.
For example, it's pretty routine that if you really want to optimize Java byte codes, you basically do some reverse engineering to translate them backwards from a machine-code like representation, and turn them back into SSA instructions (or something similar)2. You then manipulate the SSA instructions to do your optimization, then translate from there to something that targets the architecture you really care about. Even with this rather complex process, however, some concepts that are foreign to Java are sufficiently difficult to express that it's difficult to translate from some source languages into machine code that runs (even close to) optimally on most typical machines.
If you're asking about why to use intermediate representations in general, two major factors are:
- Reduce an O(N * M) problem to an O(N + M) problem, and
- Break the problem up into more manageable pieces.
If you're asking about the specifics of the Java byte codes, and why they chose this particular representation instead of some other one, then I'd say the answer largely comes back to their original intent and the limitations of the web at the time, leading to the following priorities:
- Compact representation.
- Quick and easy to decode and execute.
- Quick and easy to implement on most common machines.
Being able to represent many languages or execute optimally on a wide variety of targets were much lower priorities (if they were considered priorities at all).
- So why is P-system mostly forgotten? Mostly a pricing situation. P-system sold pretty decently on Apple II's, Commodore SuperPets, etc. When the IBM PC came out, P-system was a supported OS, but MS-DOS cost less (from most people's viewpoint, was essentially thrown in for free) and quickly had more programs available, since it's what Microsoft and IBM (among others) wrote for.
- For example, this is how Soot works.
In addition to the advantages that other people have pointed out, bytecode's a lot smaller, so it's easier to distribute and update and takes up less space in the target environment. This is especially important in heavily space-constrained environments.
It also makes it easier to protect copyrighted source code.
The sense is that compiling from byte code to machine code is faster than interpreting your original code to machine code just in time. But we need interpretations to make our application cross-platform, because we want to use our original code on every platform without changes and without any preparations(compilations). So first javac compiles our source to byte code, then we can run this byte code anywhere and it will be interpreted by Java Virtual Machine to machine code more quickly. The answer: it saves time.
Originally, the JVM was a pure interpreter. And you get the best performing interpreter if the language that you are interpreting is as simple as possible. That was the goal of the byte code: To provide an efficiently interpretable input to the run-time environment. This single decision placed Java closer to a compiled language than to an interpreted language, as judged by its performance.
Only later on, when it came apparent that the performance of the interpreting JVMs still sucked, did people invest the effort to create well-performing just-in-time compilers. This somewhat closed the gap to faster languages like C and C++. (Some Java inherent speed issues remain, though, so you will probably never get a Java environment that performs as well as well written C code.)
Of course, with the just-in-time compiling techniques at hand, we could go back to actually distributing source code, and just-in-time compiling it to machine code. However, this would heavily decrease startup performance until all relevant parts of the code are compiled. The byte code is still a significant help here because it's so much simpler to parse than the equivalent Java code.
Text Source Code is a structure that intends to be easy to be read and modified by a human.
Byte code is a structure that intends to be easy to be read and executed by a machine.
Since all the JVM does with code is read and execute it, byte code is a better fit for consumption by the JVM.
I notice that there haven't been any examples yet. Silly Pseudo Examples:
//Source code i += 1 + 5 * 2 + x; // Byte code i += 11, i += x ____ //Source code i = sin(1); // Byte code i = 0.8414709848 _____ //Source code i = sin(x)^2+cos(x)^2; // Byte code (actually that one isn't true) i = 1
Of course byte code is not just about optimizations. A large part of it is about being able to execute code without having to care about complicated rules, like checking if the class contains a member called "foo" somewhere further down in the file when a method refers to "foo".