The last two decades, I would say, have experienced a revolution in the way people think about computer programming. This revolution emanated out of the development and adoption of, moderately to extremely, rich programming languages. Richness spans from something as simple as platform-independence, to something as complex as dynamic typing. Irrespective of the amount of wealth hidden in them, these programming languages have often mandated a robust runtime environment to bolster an effective use of their wealth. Undoubtedly, Java has led this bandwagon and the Java Runtime Environment (JRE) has been constantly maturing to support this phenomenal programming language.
Java gets most of its applauses for the fact that it is platform-independent. Java code, when compiled, is transformed into something called bytecode. Bytecode is a sequence of software instructions (the Java Virtual Machine has its own, well-defined and universal instruction set; just as how a hardware platform would have). The Java methods that you write are visible to the JRE as bytecode. It is this universally accepted bytecode that imparts the platform-independence to Java. The JRE has a stack-based interpreter that processes one bytecode instruction at a time. Bytecode instructions can be as simple as an add and as complex as a tableswitch - an instruction that represents a switch statement. While executing a method, the interpreter loops through bytecode, triggering computations. The interpreter is an abstract execution engine that wraps around the physical machine - hardware bundled with the operating system.
The interpreter hence forms an additional layer of abstraction between the application and the underlying physical machine. Other than platform-independence, this layer also allows the JRE to effectively exercise runtime control over the execution. I shall delve deeper into this in future. For now, it is important to realize that this layer introduces an operational delay and decreases the throughput of the application. This is a side-effect of interpretation. The best approach to work around this side-effect is dynamic compilation or Just-In-Time(JIT) compilation.
Just-In-Time (dynamic) compilation transforms method bytecode into machine code. The compilation is called dynamic because it happens during application execution, unlike the classical static compilation of C/C++ programs. The unit of JIT compilation is a method. Not an entire class! JIT compilation picks up methods, at runtime, and translates their bytecode into machine instructions. All the subsequent invocations of this method, will not result into bytecode interpretation, but into machine-level execution (just like your statically compiled C/C++ program). The additional interpreter layer is hence peeled off! This leads to a large improvement in the method execution times and increases the application throughput. But there is a certain cost involved in JIT compilation.
JIT compilation happens parallel to application execution. So, is it an overhead to the application ? Not exactly. JIT compilation is an investment, the return of which is execution speed caused by the peel-off of the interpreter layer. The best candidates for JIT compilation are methods which have been extensively interpreted. They have reached a stage where they deserve to be promoted to machine code. A small investment in the form of compilation will lead to a huge return in the form of an increased speed of execution. But there is a risk involved here. What if a JIT compiled method was never used again ? Will our investment not go in vain ? Yes, it will. So, the JIT compiler should make all the attempts to maximize the returns on this investment. This is possible simply by compiling the right methods, at the right time, in the right way! This is precisely what the IBM J9 Just-In-Time compiler achieves. I plan to discuss how it does so in the subsequent posts.