JVM Architecture

JVM stands for Java Virtual Machine. It consists of three different components.

  1. 1. Class Loader
  2. 2. Runtime memory
  3. 3. Execution engine

1. Class Loader

When you compile a .java source file, it is converted into byte code as a .class file. When you try to use this class in your program, the class loader loads it into the main memory.

The first class to be loaded into memory is usually the class that contains the main() method.

There are three phases in the class loading process: loading, linking, and initialization.


Loading

Loading involves taking the binary representation (bytecode) of a class or interface with a particular name, and generating the original class or interface from that.

There are three built-in class loaders available in Java:

  1. Bootstrap Class Loader - It is the superclass of Extension Class Loader and loads the standard Java packages like java.lang, java.net, java.util, java.io, and so on. These packages are present inside the rt.jar file and other core libraries present in the $JAVA_HOME/jre/lib directory.
  2. Extension Class Loader - This is the subclass of the Bootstrap Class Loader and the superclass of the Application Class Loader. This loads the extensions of standard Java libraries which are present in the $JAVA_HOME/jre/lib/ext directory.
  3. Application Class Loader - This is the final class loader and the subclass of Extension Class Loader. It loads the files present on the classpath. By default, the classpath is set to the current directory of the application. The classpath can also be modified by adding the -classpath or -cp command line option.

The JVM uses the ClassLoader.loadClass() method for loading the class into memory. It tries to load the class based on a fully qualified name. If a parent class loader is unable to find a class, it delegates the work to a child class loader. If the last child class loader isn't able to load the class either, it throws NoClassDefFoundError or ClassNotFoundException>.

Linking

After a class is loaded into memory, it undergoes the linking process. Linking a class or interface involves combining the different elements and dependencies of the program together.

  1. Verification: This phase checks the structural correctness of the .class file by checking it against a set of constraints or rules. If verification fails for some reason, we get a VerifyException.
    For example, if the code has been built using Java 11, but is being run on a system that has Java 8 installed, the verification phase will fail.

  2. Preparation: In this phase, the JVM allocates memory for the static fields of a class or interface, and initializes them with default values.
    For example, assume that you have declared the following variable in your class:
        
            private static final boolean enabled = true;
        
    
    During the preparation phase, JVM allocates memory for the variable "enabled" and sets its value to the default value for a boolean, which is false.

  3. Resolution: In this phase, symbolic references are replaced with direct references present in the runtime constant pool.
    For example, if you have references to other classes or constant variables present in other classes, they are resolved in this phase and replaced with their actual references.
Initialization

Initialization involves executing the initialization method of the class or interface. This can include calling the class's constructor, executing the static block, and assigning values to all the static variables. This is the final stage of class loading.

2. Runtime memory

Method Area:

All the class level data such as the run-time constant pool, field, and method data, and the code for methods and constructors, are stored here. If the memory available in the method area is not sufficient for the program startup, the JVM throws an OutOfMemoryError.

    
        public class Employee {

            private String name;
            private int age;
            
            public Employee(String name, int age) {
            
                this.name = name;
                this.age = age;
            }
            }
    

In this code example, the field level data such as name and age and the constructor details are loaded into the method area. The method area is created on the virtual machine start-up, and there is only one method area per JVM.

Heap Area:

All the objects and their corresponding instance variables are stored here. This is the run-time data area from which memory for all class instances and arrays is allocated.

    
        Employee employee = new Employee();
    

In this code example, an instance of Employee is created and loaded into the heap area. The heap is created on the virtual machine start-up, and there is only one heap area per JVM.

Stack Area:

Whenever a new thread is created in the JVM, a separate runtime stack is also created at the same time. All local variables, method calls, and partial results are stored in the stack area. JVM can throws a StackOverflowError if the required size is larger than available.

Program Counter (PC) Registers

The JVM supports multiple threads at the same time. Each thread has its own PC Register to hold the address of the currently executing JVM instruction. Once the instruction is executed, the PC register is updated with the next instruction.

Native Method Stacks

The JVM contains stacks that support native methods. These methods are written in a language other than the Java, such as C and C++.

3. Execution Engine

Once the bytecode has been loaded into the main memory, and details are available in the runtime data area, the next step is to run the program. The Execution Engine handles this by executing the code present in each class.

However, before executing the program, the bytecode needs to be converted into machine language instructions. The JVM can use an interpreter or a JIT compiler for the execution engine.


Interpreter

The interpreter reads and executes the bytecode instructions line by line. Due to the line by line execution, the interpreter is comparatively slower. Another disadvantage of the interpreter is that when a method is called multiple times, every time a new interpretation is required.

JIT Compiler

The JIT Compiler overcomes the disadvantage of the interpreter. The Execution Engine first uses the interpreter to execute the byte code, but when it finds some repeated code, it uses the JIT compiler. The JIT compiler then compiles the entire bytecode and changes it to native machine code. This native machine code is used directly for repeated method calls, which improves the performance of the system.

The JIT Compiler has the following components:

  1. 1. Intermediate Code Generator - generates intermediate code
  2. 2. Code Optimizer - optimizes the intermediate code for better performance
  3. 3. Target Code Generator - converts intermediate code to native machine code
  4. 4. Profiler - finds the hotspots (code that is executed repeatedly)
Garbage Collector

The Garbage Collector (GC) collects and removes unreferenced objects from the heap area. It is the process of reclaiming the runtime unused memory automatically by destroying them.

Garbage collection makes Java memory efficient because because it removes the unreferenced objects from heap memory and makes free space for new objects. It involves two phases:

  1. 1. Mark - in this step, the GC identifies the unused objects in memory
  2. 2. Sweep - in this step, the GC removes the objects identified during the previous phase

Garbage Collections is done automatically by the JVM at regular intervals and does not need to be handled separately. It can also be triggered by calling System.gc().

Java Native Interface (JNI)

It is necessary to use native (non-Java) code (for example, C/C++). This can be in cases where we need to interact with hardware, or to overcome the memory management and performance constraints in Java. Java supports the execution of native code via the Java Native Interface (JNI).

Common JVM Errors

  1. 1. ClassNotFoundExcecption - his occurs when the Class Loader is trying to load classes using Class.forName(), ClassLoader.loadClass() or ClassLoader.findSystemClass() but no definition for the class with the specified name is found.
  2. 2. NoClassDefFoundError - This occurs when a compiler has successfully compiled the class, but the Class Loader is not able to locate the class file at the runtime.
  3. 3. OutOfMemoryError - This occurs when the JVM cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector.
  4. 4. StackOverflowError - This occurs if the JVM runs out of space while creating new stack frames while processing a thread.