What is Java virtual machine?
The Java Virtual Machine (JVM) serves as a runtime environment for executing Java bytecode, adhering to a standardized specification. As the name suggests, the JVM operates as a virtual processor, enabling platform-independent execution of Java programs. The cornerstone of Java's platform independence lies in its JVM, which facilitates seamless operation by being cognizant of platform-specific nuances, such as instruction lengths and other intricacies, ensuring consistent behavior across diverse operating systems. This fundamental feature empowers Java to be a versatile and cross-platform language, simplifying the development and deployment process for developers. The JVM performs following operation:
- Loads code
- Verifies code
- Executes code
Unlike many other programming languages, the Java compiler generates bytecode that is independent of any specific Operating System. When executing a Java program, it operates as a thread within the Java Virtual Machine (JVM) process. The JVM takes on the crucial tasks of loading class files, verifying the code, interpreting it, and executing it. When you initiate a command like "java," the JVM loads the class definition corresponding to the specified class and invokes its main method to initiate the program's execution. This design approach enables Java programs to be platform-independent, as the same bytecode can be executed by any JVM on various operating systems, ensuring the "Write Once, Run Anywhere" principle of Java.
The JVM plays a crucial role in achieving Java's platform independence. When you compile your Java source code, it is transformed into platform-neutral bytecode. This bytecode is then interpreted by the JVM, which is responsible for executing the code on any target operating system. The JVM can further optimize the execution by using a just-in-time compiler to convert bytecode into native machine code, caching it for improved performance. As a result, the JVM handles the translation of platform-neutral bytecode into platform-specific machine code and manages the underlying operating system calls, allowing Java programs to run seamlessly on different platforms. This abstraction provided by the JVM is what enables the "Write Once, Run Anywhere" capability in Java.
Java's memory management and thread handling are key features provided by the JVM. When you create objects or primitives in Java, the JVM allocates memory for them. Java also includes a Garbage Collector (GC) that automatically identifies and reclaims memory of objects that are no longer referenced, freeing up memory for other objects. This automatic memory management relieves developers from the burden of manual memory allocation and deallocation, reducing the risk of memory leaks and improving overall program stability.
Additionally, the JVM acts as an intermediary between the Java application and the underlying operating system. It provides interfaces to various subsystems managed by the OS, such as display, mouse, keyboard, file system, and I/O ports. This abstraction shields the Java developer from dealing with low-level system interactions, making it easier to write platform-independent code.
Every Java application operates within the context of a runtime instance of a specific concrete implementation of the abstract Java virtual machine specification. There are three notions of JVM: specification, implementation, and instance.
- Specification : A document that describes what is required of JVM Implementation.
- Implementation: Known as JRE(Java Run Time Environment.)
- Instance: Whenever you will run a java class file an instance of JVM is created.
As shown in picture, JVM is divided into three main subsystems:
- Class Loader Subsystem
- Runtime Data Area
- Execution Engine
Class Loader Subsystem
The Java virtual machine possesses a versatile Class Loader architecture, enabling Java applications to load classes in custom manners. Within a JVM, every class is loaded by an instance of java.lang.ClassLoader. Classloaders are specific Java class files designed to handle the loading of other classes onto a Java Virtual Machine. When a Java class is invoked and requires execution on a Java Virtual Machine, a designated classloader searches for and retrieves the relevant Java class, extracting its bytecode from the file system, and proceeds to execute it on the Java Virtual Machine.
The Java Class Loader Subsystem plays a crucial role in the dynamic loading, linking, and initialization of class files when they are first referenced at runtime. Its primary responsibility lies in loading class files from various sources, such as the file system or network. Java employs three default class loaders: Bootstrap, Extension, and System (or Application) class loaders, each with distinct responsibilities in managing class loading during program execution.
Boot Strap class Loader
Upon JVM initialization, the Bootstrap or Primordial classloader, a platform-specific machine code, is responsible for loading the system classloader. This crucial process sets in motion the entire classloading mechanism. Additionally, the Bootstrap classloader is responsible for loading essential components of the Java Runtime Environment (JRE), including classes from the java.util and java.lang packages, thus forming the foundation for Java application execution.
The Extension class loader in Java is responsible for loading classes from the JRE's extension directories, such as lib/ext directories. This class loader delegates class loading requests to its parent, the Bootstrap class loader. If unsuccessful, it proceeds to load classes from the jre/lib/ext directory or any other directory specified by the java.ext.dirs system property. The Extension class loader is implemented by the sun.misc.Launcher$ExtClassLoader in the JVM.
System/Application Class Loader
The System/Application Class Loader in Java is responsible for loading classes from the application's classpath and any additional paths specified in the environment variables. When a Java program is executed, the System Class Loader is the default class loader used by the JVM to load classes required by the application. It searches for classes in the directories and JAR files specified in the classpath, as well as any other paths provided through environment variables. This class loader is implemented by the sun.misc.Launcher$AppClassLoader in the JVM.
Classloader - Linking
Linking is an essential step in the process of loading classes into the Java Runtime System, which enables the JVM to use the loaded bytecodes. The linking process consists of two main steps: verification and preparation.
- Verification: During verification, the JVM ensures that the bytecode of the loaded class is well-formed and adheres to the rules and constraints of the Java language specification. It checks for various safety and security constraints to prevent any potential runtime errors or security breaches.
- Preparation: After verification, the JVM prepares the class for execution by allocating memory for its static variables and initializing them with default values. It also sets up data structures needed for method dispatch and other runtime operations.
Additionally, linking involves resolving symbolic references to other classes or interfaces that the loaded class depends on. This process resolves these references to their actual memory locations, allowing the JVM to access them during execution.
Initialization is the last phase of class loading in Java, where the static variables of the class are assigned their original values, and any static blocks present in the class are executed.
During initialization, the JVM ensures that all static variables are properly initialized with their default values or explicitly defined initial values in the code. For example, if a static variable is declared as static int count = 0;, its value will be set to 0 during initialization.
In addition to initializing static variables, any static blocks present in the class are executed. Static blocks are used to perform additional setup or initialization tasks that are required before the class is used. They are executed in the order they appear in the code.
Initialization is a crucial step in the class loading process as it ensures that all necessary static data is set up correctly before any instances of the class are created or any static methods are invoked. It provides a stable and consistent state for the class and its static members during the runtime execution of Java programs.
Runtime Data Areas
The Java Virtual Machine (JVM) encompasses multiple run-time data areas that play essential roles during program execution. Upon JVM start-up, certain data areas are initialized and persist until the JVM terminates, while others are specific to individual threads. Per-thread data areas are dynamically created when a new thread is spawned and cease to exist upon the thread's completion. These distinct data areas facilitate efficient memory management and ensure proper isolation of data between threads, contributing to the smooth execution of Java programs.
The Method Area is a memory region in the Java Virtual Machine that is shared among all threads, akin to the Heap. It is established during JVM initialization and houses compiled code, methods, associated data, and fields. Additionally, the Method Area incorporates the runtime constant pool, which contains symbolic references, literals, and other runtime-specific constants used by the program. This shared memory space ensures efficient storage and access to code and data across threads, promoting optimal performance and resource utilization.
The Heap Area is a crucial memory region in the Java Virtual Machine responsible for storing objects and their instance variables. Whenever an object is instantiated in Java, it is allocated memory in the heap. This memory region is dynamic and can grow or shrink as needed to accommodate the creation and destruction of objects during program execution. Proper management of the heap is essential to ensure efficient memory utilization and prevent memory leaks, as objects are automatically garbage collected when they are no longer reachable or referenced by the program.
The Stack Area is another important memory region in the Java Virtual Machine, used to manage method invocations and local variables. Each time a method is invoked, a new frame is created and pushed onto the stack to store the method's parameters, local variables, and intermediate calculation results. As the method execution completes, the frame is popped off the stack, and control returns to the calling method. The stack follows a Last-In-First-Out (LIFO) order, meaning that the most recently called method's frame is at the top of the stack. Since the stack is responsible for managing method calls and local variables, it is relatively fast and has limited memory space compared to the heap.
The Program Counter (PC) Register is a vital component in the Java Virtual Machine responsible for keeping track of the execution of instructions within a thread. It holds the memory address of the currently executing instruction, allowing the JVM to properly sequence and manage the instructions within a method. As the thread moves through the execution of a method, the PC Register gets updated to point to the next instruction to be executed.
The PC Register plays a crucial role in the control flow of the thread, determining the sequence in which instructions are processed. However, it is important to note that the PC Register is not used for native methods, as native methods are executed by the underlying operating system or hardware, and their instructions do not reside within the JVM.
Native Method Stack
The Native Method Stack, also known as the C-Stack, is a separate area of memory associated with each thread in the Java Virtual Machine (JVM). It is used specifically for executing native methods, which are methods written in languages other than Java, such as C or C++.
Native methods cannot be directly executed by the JVM as they are platform-specific and require direct interaction with the underlying operating system or hardware. Therefore, the Native Method Stack is used to handle the execution of these methods. It provides the necessary space for the native code to execute, manage local variables, and handle the call and return operations specific to native methods.
Unlike the Java stack, which is used for executing Java bytecode and managing Java method calls, the Native Method Stack has a different purpose and is reserved for native method execution. By maintaining separate stacks for Java and native methods, the JVM can effectively handle the execution of both types of methods within the same program.
The Execution Engine is a fundamental component of the Java Virtual Machine (JVM), responsible for executing the bytecode assigned to the runtime data areas by the class loader. It serves as the communication link between the various memory areas of the JVM, allowing seamless interaction with the Method Area, Heap, Stack, and Native Method Stack. Each running Java application comprises multiple threads, and each thread represents a distinct instance of the execution engine, enabling concurrent and efficient execution of Java bytecode.
- JIT Compiler
- Garbage Collector
The Interpreter is a crucial component of the Java Virtual Machine responsible for reading, interpreting, and executing bytecode instructions sequentially. It processes each bytecode instruction one by one, resulting in quick interpretation but relatively slower execution of the interpreted code. This is a limitation of interpreted languages. Bytecode, which serves as an intermediate representation of Java code, behaves similar to an interpreter, facilitating cross-platform portability and enabling Java's "Write Once, Run Anywhere" paradigm.
The JIT (Just-In-Time) compiler plays a significant role in the Java Virtual Machine by transforming bytecode into an intermediate-level expression called IR (Intermediate Representation) for optimization purposes. Subsequently, it further converts the IR into native code, improving overall performance compared to pure interpretation. The JIT compiler is a valuable addition that addresses the limitations of the interpreter, aiming to enhance execution speed. It efficiently manages method counts, allowing it to prioritize optimization efforts on frequently used methods and thus boost the overall efficiency of Java programs.
Garbage collection (GC) is a crucial process within the Java virtual machine (JVM) responsible for freeing up memory occupied by objects that are no longer reachable and referenced by any other Java object. As Java objects are dynamically allocated memory when created, the Garbage Collection process plays a vital role in reclaiming memory once these objects are no longer needed. By identifying and collecting objects that are no longer reachable, the Garbage Collector efficiently manages live objects while identifying and eliminating unreferenced objects, effectively freeing up memory for future use. More about.... Java Garbage Collection Basics
Native Method Interface
Native methods in Java provide a way to incorporate code written in other languages, like C or C++, into your Java code. They are utilized when Java's standard functionality does not offer the required features or capabilities for a particular task. By using native methods, developers can bridge the gap between Java and other programming languages, allowing them to use existing libraries, access low-level system resources, or perform computationally intensive tasks more efficiently. This flexibility enables Java to interact seamlessly with platform-specific or performance-critical operations, making it a versatile language for a wide range of applications.
Native Method Libraries
Native Method Libraries are a collection of native libraries that are required for the execution engine of the Java Virtual Machine (JVM). These libraries contain the implementation of native methods, which are methods written in other languages like C or C++ and are used to perform tasks that cannot be directly accomplished within the Java environment. When a Java program calls a native method, the JVM uses these native method libraries to execute the corresponding native code. This allows Java to access system-level functionality, interact with hardware, or utilize external libraries written in other languages, enhancing its capabilities and versatility.
The Java Virtual Machine (JVM) is a crucial component of the Java platform that provides a runtime environment for Java programs to execute. It acts as an interpreter for Java bytecode, allowing Java programs to be platform-independent and run on any system with a compatible JVM. The JVM also manages memory, loads classes, and performs various optimizations, making it an essential part of the Java programming ecosystem.