Complied and Interpreted
In my previous post, we have seen how java is so flexible and simple, while very small and yet equipped with powerful features like Object Oriented principles (OOPS). Now in this post we examine one of the important features of java i.e. how java is compiled and interpreted.
Here lies the major difference between Java and languages such as C++, Pascal, and many others. These languages compile their source program into object code (an .obj file). The linker converts the generated object code into an executable (.exe file) by combining object code with the desired libraries. When we run the EXE, the loader in the operating system loads the executable code in memory, resolves the function references with the absolute memory addresses, and executes the code. Compiling and running a Java program differs substantially from the preceding procedure. As explained earlier, a Java compiler translates the Java source program into what is known as bytecode, which is the set of instructions for a virtual CPU. No linker process is involved when we create an executable from a Java source program. In fact, the only executable that is created from a Java source program is the bytecode. So how does this bytecode run on a real CPU? When we run a Java executable (bytecode) on our machine, an interpreter converts each bytecode instruction into a real CPU instruction. This instruction then executes on the real CPU, so compiling and running a Java program involves both compilation and interpretation processes. Thus, Java is considered both compiled and interpreted.
As Java bytecode is interpreted into machine language instructions at runtime, Java code execution suffers in performance. To overcome this performance limitation, most JVMs implement a number of code optimization techniques, one of which is the use of a Just-in-Time (JIT) compiler that translates the bytecode into machine language code before the real CPU begins the program execution. Obviously, the JIT compiler cannot perform as many optimizations as an offline compiler such as the C++ compiler, because it has to translate the bytecode into machine language code in real time. If the JIT compiler were to attempt to do so, it would take a long time to start the program after the user fires it up from the command line. We have to consider the trade-off between start-up time and throughput. VMs employ a variety of optimization techniques these days. Most of them can be tuned to prefer start-up over throughput, or how quickly they adapt the optimizations. There’s a whole body of expertise on tuning VMs for performance, and for many applications, the Java version is as quick as (and sometimes quicker than) the C++ counterpart.
Another important feature introduced in Java is its dynamic nature. As we have seen, a Java source program compiles into bytecode. This bytecode is stored in a file with the extension .class.
A running Java application can load a compile-time unknown .class file, understand the defined class, instantiate it, and use it for its own purpose. This process is called “introspection and reflection” in Java. A running Java program can introspect an unknown class, understand the attributes and operations defined in it, create an instance of the class, set the attributes of the created object, and invoke the member functions on the created object. It can also create an array of objects of this unknown type at runtime. This is the dynamic nature of Java.