JVM (Java Virtual Machine): In this tutorial on JVM (Java Virtual Machine), we will learn the fundamental aspects of Java Programming. Our focus will be on understanding the architecture of JVM and its various subsystems. Furthermore, we will explore the concept of Java Virtual Machine itself and gain insights into its functioning.
Additionally, we will look in-depth at Java Classloader, a pivotal component of Java’s execution, the Java Memory Model, and provide a detailed examination of the JVM screen.
What is JVM In Java?
The Java Virtual Machine (JVM) is a fundamental component of the Java platform. It is a software-based execution environment responsible for compiling the (.java file) and converting that into the byte code (.class file). JVM is a part of Java Runtime Environment(JRE).
Java applications follow the principle of WORA, which stands for “Write Once, Run Anywhere“. This fundamental concept implies that a developer can create Java code on one system and confidently anticipate it functioning on any other Java-enabled system without requiring any modifications.
The seamless portability of Java applications across different platforms owes its success to the Java Virtual Machine (JVM).
JVM architecture is the structural framework that underlies the execution of Java applications. It comprises key components like the class loader, memory areas, execution engine, and native interface. The class loader loads Java classes into memory, while memory areas manage data and code storage.
The execution engine interprets and executes bytecode instructions, and the native interface facilitates interaction with native libraries. This architecture enables Java programs to run on diverse platforms, maintaining the language’s platform independence while ensuring efficient execution.
Now, let us try to understand the JVM architecture in more detail. Let me inform you that we have mainly divided the JVM architecture into mainly 3 subsystems that are:
Now, we will try to understand each of these subsystems in detail one by one, with their use cases and functionality:
The First Subsystem of JVM architecture is ClassLoader, and it is also part of the Java Runtime Environment (JRE). Whose jobs dynamically load the class file from various sources to the Java Virtual machine. The source may be a File system, Network, or any other source.
ClassLoader has mainly 3 phases:
The loading phase is loading the byte code from various sources like Network Sockets, fileSystem, and jar files into the memory.
Again, in the Loading phase, we have 3 different types of classloaders are there:
- Bootstrap Class Loader: This will help you load the JDK internal classes, rt.jar, and other core classes like java.lang.* package classes.
- Extensions Class Loader: This will help you load the classes from the JDK extension directory, which is $JAVA_HOME/lib/ext directory.
- Application Class Loader: This loads classes from the current classpath, and we can also set the classpath by passing the -cp or -classpath command in the command line while invoking a program.
How Does the ClassLoader Work in Java?
When the Java Virtual Machine (JVM) needs to access a class, the JVM initiates the process by invoking the loadClass() method from the java.lang.ClassLoader class. Then, internally, the loadClass() method calls the findLoadedClass() method to check whether the requested class is already loaded. Doing this helps in avoiding loading the same class multiple times.
If the class is not loaded yet, JVM delegates or requests the parent ClassLoader to attempt to load the class. If the parent ClassLoader cannot locate the class, the findClass() method is invoked to search for the class in the file system. For Better Understanding, you can check the diagram below.
This process of ClassLoader delegation ensures efficient class loading in Java. To understand it better, we have devised a simple example to try to follow this for better understanding.
Suppose there is a requirement for an application-specific class called “Student.class.” Initially, the request to load this class is handled by the Application ClassLoader/System ClassLoader, which then delegates it to the Extension ClassLoader and the Bootstrap ClassLoader.
If the class is not found in the standard Java libraries (rt.jar), the Extension ClassLoader looks for it in the “jre/lib/ext” or extension directory. If the class is found there, the Extension ClassLoader loads it. The Application ClassLoader only loads the class if the Extension ClassLoader does not find it.
If the Application class loader can also not find the requested class, then ClassNotFoundException is thrown.
According to the visibility principle, child ClassLoaders can access classes loaded by parent ClassLoaders, but the reverse is not true. If the Application ClassLoader loads “Student.class,” attempting to explicitly load “Student.class” using the Extension ClassLoader will result in a java.lang.ClassNotFoundException.
During the linking phase, the JVM ensures the already loaded classes, interfaces, and its parent classes after the loading phase.
The Linking phase is also divided into 3 phases:
Verify: During this phase, JVM checks or ensures the correctness of the .class file(bytecode), like whether the file is properly formatted and generated by a valid compiler. A few other checks are mentioned below:
- Ensure variables are initialized before usage.
- Ensure final classes are not sub-classed and final methods are not overridden.
- Ensure methods and classes respect access rules and methods are invoked with the correct number and type of parameters and return values.
- Variables and classes are assigned and initialized with values of the correct type.
If the above checks fail, we will get a runtime exception java.lang.VerifyError, and the entire process will halt. The class file will be ready for compilation after completing the verification process.
Prepare: The prepare phase is executed after the verification phase’s completion. In this phase, the static fields of a class or interface are created and initialized static fields with the default value.
For Example, int fields are initialized by 0, and objects are initialized with the NULL value. It’s important to note that during the preparation phase, the execution of static blocks does not occur; this responsibility falls within the Initialization phase.
Resolve: During the Resolve phase, symbolic references within the bytecode are substituted with real references. This is accomplished by searching the Method Area’s Runtime Constant Pool.
In this phase, the initialization process for each class takes place. It initializes all static variables with their designated values and executes the static block for each class.
Runtime Data Area (Memory Area)
The Runtime Data Area serves as the memory region within the JVM and works as a storage area for the class and instance data. There are five components available inside the Runtime Data Area, and that is:
Method Area: In Java virtual machine, we have a method area accessible to all threads within the JVM; that’s why the method area is not thread-safe. and it will be created during the JVM’s startup process.
All the class-level data, such as the run-time constant pool, field, method data, and the code for methods and constructors, are stored inside the method area. During the program startup, if there is not enough memory left, then in that case, you will get the OutOfMemoryError.
Heap Area: Like the Method area for every JVM, one heap area is available. This heap area is established during the JVM startups. Inside the heap area, all the objects and their corresponding instance variables are stored here.
Multiple threads can access the Heap area at a time; because of this reason, the data stored in heap memory is not thread-safe.
Stack Area: Every thread gets its own stack; hence, the data we store inside the stack are thread-safe. When you use a method, a special space is reserved in this stack, which we call a “Stack Frame”.
The memory spaces where Java methods run are called Java stacks in Java. Within these stacks, individual frames are set up for method execution. A fresh frame is established in the stack whenever a method is called. Once the method’s work is done, its corresponding frame is removed or “destroyed” from the stack.
Program Counter (PC) Register: Every thread maintains its own PC Registers, which store the address of the currently executing instruction. After an instruction is executed, the PC register is updated to point to the next instruction in line.
Native Method Stack: In a Java program, Java methods run on Java stacks, while native methods run on Native method stacks.
To execute the native methods, we need the Java native methods libraries, and to connect native methods libraries header files with the Java program, we need the Native method interface.
The main functionality of the Execution Engine is to run the bytecode, which is available in the Runtime Data Area. While executing those bytecodes, the Execution Engine reads and executes each bytecode piece by piece.
The Execution Engine mainly consists of three primary elements: Interpreter, JIT Compiler, and Garbage Collector. and we will discuss each of them one by one:
The interpreter operates by reading and executing bytecode instructions line by line, resulting in relatively slower performance. Another disadvantage of an Interpreter is if a method is called multiple times each time a new interpretation is required.
JIT Compiler (Just In Time Compiler)
It is employed to enhance the efficiency of an interpreter. This process involves compiling the entire bytecode into native code. As a result, when the interpreter encounters repeated method calls, the Just-In-Time compiler supplies direct native code for that section, eliminating the need for re-interpretation and improving overall efficiency.
The gathering and disposal of no longer referenced objects is known as the Garbage Collection. However, you can initiate garbage collection by invoking System.gc(), there is no assurance of immediate execution. The JVM’s garbage collection system is responsible for identifying and cleaning up unused objects.
The Java Virtual Machine (JVM) is the backbone of Java’s cross-platform compatibility and runtime execution. It plays a pivotal role in translating Java code into executable actions, providing a versatile and secure environment for running Java applications.
The JVM’s capabilities are integral to Java’s widespread popularity and adaptability, from managing memory and handling bytecode to supporting multithreading and ensuring security. Its ability to balance performance with platform independence, aided by components like the method area, heap, and runtime stack, makes it a cornerstone of modern software development.
As Java continues to evolve, the JVM remains a vital component, fostering the development of robust, scalable, and portable applications across diverse computing environments.
Your feedback matters! If you have any doubts or valuable suggestions to enhance our article on the Java Virtual Machine (JVM), we encourage you to share them in the comment section below. Your insights and questions not only help clarify concepts but also contribute to improving the quality of information we provide. So, don’t hesitate to engage with us; your comments are highly valued!