The Java Virtual Machine
Just-in-time compilation translates Java bytecode into native machine language during program execution.
The Assumption That Bytecode Must Be Slow
An early assumption about Java went like this: if a program is written as bytecode and an interpreter executes that bytecode, it can never be as fast as a program that has already been compiled into the computer's native machine language. That assumption points at a real performance concern. It leaves out an important part of how Java execution can work, and this article follows the mechanism that fills that gap.
To understand the mechanism you need three pieces: the bytecode a Java program is compiled into, the Java Virtual Machine that runs that bytecode, and the two ways the JVM can carry out bytecode. We will trace what happens to the same piece of a program as it runs, first under direct interpretation and then under just-in-time compilation.
Bytecode and the Virtual Computer
Java bytecode is the machine language of the Java Virtual Machine, and it is produced by compiling Java programs.
The Java Virtual Machine, or JVM, is a virtual computer rather than a physical machine. It runs Java bytecode.
Picture preparing one Java program for several different types of computers. Java does not ask every computer to understand the same physical machine language directly. Instead, the program is compiled into Java bytecode, a machine language designed for a computer that is virtual rather than physically present. The physical computers each provide their own way of carrying out that bytecode, and the virtual computer is the common layer that the program is written against.
From Java Source to Bytecode to Running Code
The path starts with a Java source file, which a Java compiler translates into Java bytecode. That bytecode is not yet native machine code for any particular processor. It is handed to the JVM, which runs it either by interpretation or by just-in-time compilation. The diagram below shows the stages in order, so you can see where each form of the program exists.
Direct Interpretation and Just-in-Time Compilation
A bytecode interpreter executes bytecode directly. Each time an instruction is reached, the interpreter carries out its effect. Just-in-time compilation works differently. Unlike a plain interpreter, the JVM does not only execute bytecode directly. It also translates parts of the program into native machine language while the program is running. Once a part has been translated, the translated version can execute much more quickly than the same part would through interpretation.
Just-in-time compilation translates Java bytecode into native machine language during program execution.
The flowchart compares the two approaches for one bytecode instruction that runs more than once. Under direct interpretation, the interpreter handles the instruction again on every run. Under just-in-time compilation, the instruction's part is translated once, and later runs use the native result.
Why Repetition Pays Off
The benefit of just-in-time compilation comes from repetition. A given part of a program is often executed many times as the program runs. If that part is translated into native machine language during execution, later executions of the translated part can be faster than repeatedly interpreting it. The translation itself takes place while the program runs, but the translated result can improve the program's overall execution time whenever the relevant part is used repeatedly. The source makes the key point directly: the greatest benefit occurs when a program part is executed many times.
Comparing total work for a repeated part (illustrative numbers)
A part of a program runs 100 times. The numbers below are invented for this lesson to show the shape of the trade-off. They are not measurements from any real JVM. Interpreting the part costs 10 units each run. Translating it once costs 50 units. Running the translated native version costs 1 unit each run. Compare total work for pure interpretation and for translation followed by reuse.
- 1Total work under pure interpretation
Each of the 100 runs is interpreted, so the cost is 100 times 10, which equals 1000 units.
- 2Total work under just-in-time compilation
The part is translated once for 50 units. Then the 100 runs each cost 1 unit, which is 100 units. The total is 50 plus 100, which equals 150 units.
- 3Compare the two totals
The translated version does about 150 units of work against 1000 for pure interpretation. The gain comes from paying the translation cost once and reusing the result.
With these invented numbers, the translated version does far less total work because the translation is reused across many runs. The same arithmetic shows why the benefit shrinks when a part is used only a few times.
What do you think happens?
A part of a program runs only once. Is translating it to native machine language likely to improve overall execution time?
Reveal answer
Not necessarily. The benefit of just-in-time compilation comes from repetition, so a part that runs only once gains little from being translated.
Translation work happens while the program runs, and the payoff comes when the translated result is reused. A part executed once has no later runs to benefit from that reuse.
Correcting the Always-Slower Misconception
The early assumption said Java bytecode programs can never run as quickly as programs compiled into native machine language. The correction is more precise. Interpreted Java bytecode can run slower than native machine language. But just-in-time compilation can translate parts of the bytecode into native machine language during execution, and those translated parts can then execute much more quickly than they could through interpretation. Because of this, just-in-time compilation has largely overcome the earlier performance concern and can significantly speed up a program's overall execution time.
Interpreters and Particular Computers
A Java bytecode interpreter implements the JVM for a particular type of computer. This is the link between the portable bytecode and the physical machine underneath. The bytecode itself does not change from computer to computer. What changes is the interpreter, which is written to carry out the virtual computer's instructions on the hardware it runs on.
Java achieves platform independence by pairing the same compiled bytecode with suitable interpreters on different computers. So one compiled Java program can run on several different types of computers, because each computer supplies its own suitable JVM implementation. The program is prepared once as bytecode, and the interpreter on each machine handles the connection to that machine.
Mistakes Learners Make with the JVM
Common mistake Thinking the JVM is a physical processor that Java programs are wired to
Why it failsThe JVM is a virtual computer, not a physical machine. Its bytecode is a machine language designed for a computer that is virtual rather than physically present.
Do this insteadTreat the JVM as a common virtual layer that runs bytecode, and treat the physical computer as the thing beneath it.
Common mistake Assuming that every bytecode instruction is translated to native code as soon as the program starts
Why it failsTranslation is a feature of just-in-time compilation during execution, and its payoff comes from repeated use of a part. The source does not describe a fixed moment when each part is translated.
Do this insteadFocus on the idea that repeatedly executed parts are the ones that benefit, and avoid claiming a specific timing rule the source does not give.
Common mistake Concluding that all bytecode programs are slow because a plain interpreter can be slower than native code
Why it failsThis ignores just-in-time compilation, which translates parts of the bytecode during execution so those parts can run much faster than interpretation.
Do this insteadDistinguish direct interpretation from just-in-time compilation before judging the speed of Java bytecode.
Common mistake Believing the bytecode must be rewritten for each computer, when the same bytecode can be paired with a suitable interpreter on each machine
Checking Your Understanding
A Java program contains two parts. Part A runs twice during the program. Part B runs ten thousand times. Under just-in-time compilation, which part is more likely to benefit from translation to native machine language, and why?
Hints
- Recall where the source says the greatest benefit occurs.
- Think about how many later runs can reuse a translated result.
Recap of the Mechanism
- 1Java source is compiled into Java bytecode, the machine language of the Java Virtual Machine.
- 2The JVM is a virtual computer. A bytecode interpreter implements it for a particular type of computer.
- 3Direct interpretation carries out each bytecode instruction through the interpreter, while just-in-time compilation translates parts of the program into native machine language during execution.
- 4The benefit of translation comes from repetition, since the greatest gain occurs when a part runs many times.
- 5Interpreted bytecode can be slower than native code, but just-in-time compilation can significantly speed up overall execution, so the always-slower assumption is incorrect.
Key Takeaways
- Java source is compiled into Java bytecode, the machine language of the Java Virtual Machine, which is a virtual computer.
- A bytecode interpreter implements the JVM for a particular type of computer, so one compiled program can run on different computers.
- Just-in-time compilation translates parts of bytecode into native machine language during execution, and the benefit comes from repeated execution.
- Interpreted bytecode can be slower than native code, but the always-slower assumption is a misconception because translated parts can run much faster.