Java, being a platform-independent programming language, doesn’t work on the one-step compilation. Instead, it involves a two-step execution, first through an OS-independent compiler; and second, in a virtual machine (JVM) which is custom-built for every operating system.
The two principal stages are explained below:
Principle 1: Compilation
First, the source ‘.java’ file is passed through the compiler, which then encodes the source code into a machine-independent encoding, known as Bytecode. The content of each class contained in the source file is stored in a separate ‘.class’ file. While converting the source code into the bytecode, the compiler follows the following steps:
Step 1: Parse: Reads a set of *.java source files and maps the resulting token sequence into AST (Abstract Syntax Tree)-Nodes.
Step 2: Enter: Enters symbols for the definitions into the symbol table.
Step 3: Process annotations: If Requested, processes annotations found in the specified compilation units.
Step 4: Attribute: Attributes the Syntax trees. This step includes name resolution, type checking and constant folding.
Step 5: Flow: Performs dataflow analysis on the trees from the previous step. This includes checks for assignments and reachability.
Step 6: Desugar: Rewrites the AST and translates away some syntactic sugar.
Step 7: Generate: Generates ‘.Class’ files.
Principle 2: Execution
The class files generated by the compiler are independent of the machine or the OS, which allows them to be run on any system. To run, the main class file (the class that contains the method main) is passed to the JVM and then goes through three main stages before the final machine code is executed. These stages are:
These states do include:
- ClassLoader
- Bytecode Verifier
- Just-In-Time Compiler
Let us discuss all 3 stages.
Stage 1: Class Loader
The main class is loaded into the memory bypassing its ‘.class’ file to the JVM, through invoking the latter. All the other classes referenced in the program are loaded through the class loader.
A class loader, itself an object, creates a flat namespace of class bodies that are referenced by a string name. The method definition is provided below illustration as follows:
Illustration:
// loadClass function prototype Class r = loadClass(String className, boolean resolveIt); // className: name of the class to be loaded // resolveIt: flag to decide whether any referenced class should be loaded or not.
There are two types of class loaders
- primordial
- non-primordial
The primordial class loader is embedded into all the JVMs and is the default class loader. A non-primordial class loader is a user-defined class loader, which can be coded in order to customize the class-loading process. Non-primordial class loader, if defined, is preferred over the default one, to load classes.
Stage 2: Bytecode Verifier
After the bytecode of a class is loaded by the class loader, it has to be inspected by the bytecode verifier, whose job is to check that the instructions don’t perform damaging actions. The following are some of the checks carried out:
- Variables are initialized before they are used.
- Method calls match the types of object references.
- Rules for accessing private data and methods are not violated.
- Local variable accesses fall within the runtime stack.
- The run-time stack does not overflow.
- If any of the above checks fail, the verifier doesn’t allow the class to be loaded.
Stage 3: Just-In-Time Compiler
This is the final stage encountered by the java program, and its job is to convert the loaded bytecode into machine code. When using a JIT compiler, the hardware can execute the native code, as opposed to having the JVM interpret the same sequence of bytecode repeatedly and incurring the penalty of a relatively lengthy translation process. This can lead to performance gains in the execution speed unless methods are executed less frequently.
The process can be well-illustrated by the following diagram given above as follows from which we landed up to the conclusion.
Conclusion: Due to the two-step execution process described above, a java program is independent of the target operating system. However, because of the same, the execution time is way more than a similar program written in a compiled platform-dependent program.
Implementation:
Consider simple printing program is written somewhere on the local directory in a machine.
Java
// Java Program to Illustrate Compilation and Execution // Stages // Main class class GFG { // Main driver method public static void main(String[] args) { // Print command System.out.print( "Welcome to Geeks" ); } } |
Welcome to Geeks
Let us understand the real compilation and execution process.
Step 1: Let us create a file writing simple printing code in a text file and saving it with “.java” extension.
Step 2: Open the terminal(here we are using macOS)and go to the Desktop directory using the below command as follows.
cd /Users/mayanksolanki/GFG.java
Step 3: Let us try to compile our program with the below command
javac GFG.java
Step 4: Lastly run it with the below command as follows:
java GFG
Note: GFG.class file is created after the third step which means that now our entire code in the java programming language is secure encrypted as it contains only binary. In step 4 we are running that file. Refer to the below media for ease of understanding.