The Java Virtual Machine (JVM) is an abstract computing machine with a memory-based instruction set. JVM is the foundation of the Java programming language and the reason behind Java’s cross-platform compatibility and minimal compiled code size.
JVM is a virtual machine that executes the Java byte code on an actual device (your computer). Since the JVM does not comprehend Java source code, we need a javac compiler to compile .java files into .class files that contain the byte codes. These byte codes are easily understandable by JVM.
Each Operating System has its own JVM, but the output they produce after the byte code has been executed is the same across all of them. This means that the byte code generated on macOS can be executed on Windows and vice versa. This is why Java is considered a platform-independent language (write once, run anywhere).
Every Java developer is well aware of the fact that it is the Java Runtime Environment (JRE) that executes the bytecodes. But many don’t know that JRE is an implementation of Java Virtual Machine (JVM) only, which analyses, interprets and executes the bytecode.
It is crucial to know the JVM architecture, as it enables us to write code efficiently. Let’s learn more deeply about the JVM architecture and different components of the JVM as follows:
In the coming sections, we shall study each aspect of the JVM Architecture as shown in the diagram above.
The class loader is a subsystem that handles Java’s dynamic class loading. It is mainly responsible for loading, linking and initialising the loaded class (.class file). This occurs when the loaded class is referring to another class for the first time (during runtime).
Let’s discuss the three functions of the Class Loader in detail.
Loading the compiled classes (.class files) is the primary task of the Class Loader. The three ClassLoaders that help in achieving this are BootStrap ClassLoader, Extension ClassLoader, and Application ClassLoader.
- Bootstrap Class Loader: It acts as the parent of all the other Class Loaders in Java. It loads the main JDK classes from the bootstrap classpath.
- Extension Class Loader: It passes all the class loading requests to its parent class, Bootstrap, and if unsuccessful, it loads classes using extension classpath.
- System/Application Class Loader: It loads the application-specific classes from the system classpath set while invoking a program.
Note: ClassLoaders follow the Delegation Hierarchy Algorithm while loading the .class file
Linking involves the processes of verification, preparation and resolution of the loaded classes.
- Verification: Bytecode Verifier checks whether the .class file is formatted correctly and generated by a compiler or not. If the verification fails, we get a run-time error. Once this verification is completed, the class file is sent for compilation.
- Preparation: It allocates memory and assigns default values to static variables. No code is executed here, as that is the part of initialisation.
- Resolution: Here, all symbolic references with the original references from the Method Area are replaced.
This is the final phase of loading a class. All static variables are assigned their original values defined in the code and static block. This is executed from top to bottom in a class, and the class hierarchy, from parent to child.
Runtime Data Areas
These are the memory areas assigned when the program runs on the Operating System. The Class Loader subsystem saves the name of the loaded class and its parent class and other method information. For every loaded .class file, it constructs one Class object in Heap memory to represent the file.
Later in our code, we can use this object to access class level information (class name, parent name, variable information, static variables, methods etc.)
1. Method Area: All class-level data, including methods, static variables and constant runtime pool, are stored here. Each JVM has only one method area as a shared resource, so access to the method area and dynamic linking must be safe.
2. Heap Area: All the information of objects and their instance variables are stored here. Each JVM has only one Heap area, and it is also a shared resource. The data stored is not thread-safe, as both heap and method areas share memory for numerous threads.
3. Stack: This is thread-safe as it is not a shared resource. For every thread, a separate runtime stack is created to store the method call. For every method call, one entry is made in the stack memory called Stack Frame. All local variables are created in the stack memory.
Stack Frame is divided into three parts:
- Local Variable Array: The number of local variables involved in a method and their corresponding values is stored here.
- Operand Stack: If an intermediate operation is required, this serves as a runtime workspace for the process to be completed.
- Frame Data: All symbols related to the method are stored here. For exceptions, the catch block information is maintained in the frame data.
Since they all are runtime stack frames, when a thread ends, JVM deletes its stack frame as well.
4. PC Registers: Each thread in Java has its own set of PC registers. When a JVM thread starts, a Program Counter Register is created to hold the address of the presently executing instruction. After the instruction is executed, they are updated with a new instruction.
5. Native Method Stacks: The Native Method Stack stores information about the native methods. A unique native method stack is generated for each thread. Once the native thread is created and initialised, it invokes the run() method in the JVM thread.
Uncaught exceptions (if any) are handled when the run() function returns, and the native thread checks whether the JVM needs to be terminated or not. When the thread ends, all resources for both the native and Java threads are released.
This is where the bytecode is executed. The Execution Engine reads the data assigned to the JVM memory areas and executes the instructions of bytecode line by line.
1. Interpreter: The interpreter decodes the bytecode and performs one-by-one execution of the instructions. As a result, it quickly understands one bytecode line, but executing the total interpreted result takes longer. The downside is that when a method is called several times, a fresh interpretation is required each time, resulting in slower execution.
2. Just In Time (JIT) Compiler: The interpreter’s disadvantage is mitigated by the JIT Compiler. The Execution Engine converts byte code with the help of the interpreter. Still, when it encounters a repeated code, it uses the JIT compiler, which compiles the entire bytecode and converts it to native code. This native code is then used for repeated method calls, which improves the system’s performance.
3. Garbage Collector- Garbage Collector’s primary goal is to free Heap Memory by removing unreachable things. JVM considers an object alive as long as it is referenced. The Garbage Collector eliminates an object that is no longer referenced and hence not accessible by application code and reclaims its unused memory. Garbage collection occurs automatically in most cases, but we can also start using the System.gc() function (but the execution is not guaranteed).
Java Native Interface: It is a programming framework. This interface is used to connect with Native Method Libraries (typically written in C/C++) necessary for the execution and to provide the features of such Native Libraries. This allows JVM to call C/C++ libraries and be called by C/C++ libraries that are hardware-specific.
Native Method Libraries: It is a collection of Native Libraries (C, C++) that the Execution Engine requires. They can be accessed through Java Native Libraries.
The JVM runs several threads simultaneously to accomplish each of the tasks we discussed in the JVM Architecture above. Some of these threads (application threads) carry programming logic and are produced by the program itself, while the rest of the threads (system threads) are created by JVM to perform background activities in the system.
The main application thread is generated as part of calling public static void main(String), and it is this main thread that creates all other application threads. Application threads carry out tasks such as executing instructions beginning with the main() method and generating objects in the heap memory if new keywords are found in any method logic. Some other threads are Compiler Threads, GC Threads, VM Threads etc.
Frequently Asked Questions
The Javac compiler compiles the .java files into .class files that contain the byte codes that are easily understandable by JVM. Then, JVM executes these bytecodes on a computer or any other device by loading the class using the Class Loader subsystem, assigning memory in Runtime Data Areas and then finally implementing the loaded class utilising the help of the Execution Engine.
The JVM serves two significant purposes: allowing Java applications to run on any device or Operating System (Write once, execute anywhere) and managing and optimising the program’s memory.
The three main components of JVM Architecture are:
1. Class Loader Subsystem
2. Runtime Data Area
3. Execution Engine
In this article, we have discussed JVM Architecture and its internal work. JVM offers a runtime environment in which Java byte code can be executed. Javac compiler generates bytecodes and saves them in a .class file. Then, JVM translates these bytecodes and executes them.
Class Loader subsystem is mainly responsible for Loading, Linking and Initialising the loaded class. Runtime Data Areas are the memory areas assigned when the program runs on the Operating System. Finally, the Execution Engine executes the bytecode by reading the data set to the memory areas and implementing the instructions line by line.
Don’t stop here. Check out our courses on Java Programming Language and learn more about it under the guidance of our mentors.
By Mehak Goel