Understanding JVM and Garbage Collection with a Complex Java Program

The Java Virtual Machine (JVM) is an abstract computing machine that enables a computer to run a Java program. The JVM performs various tasks such as loading class files, verifying bytecode, executing code, and managing runtime data areas. The key components of the JVM include the Class Loader, Bytecode Verifier, Just-In-Time (JIT) Compiler, and Garbage Collector.

The JVM's internal structure includes several key components that work together to execute Java programs efficiently:

  • Class Loader Subsystem: Responsible for loading class files. It uses three class loaders: Bootstrap Class Loader, Extension Class Loader, and Application Class Loader.
  • Runtime Data Areas: Divided into several memory areas:
    • Method Area (Metaspace): Stores class-level data such as class definitions, static fields, and method data.
    • Heap: The runtime data area from which memory for all class instances and arrays is allocated.
    • Java Stacks: Each thread has its own stack, which stores frames. A frame contains local variables, an operand stack, and a reference to the constant pool.
    • Program Counter (PC) Register: Contains the address of the currently executing instruction of the thread.
    • Native Method Stack: Contains all native method information.
  • Execution Engine: Executes the bytecode. It consists of:
    • Interpreter: Interprets bytecode one instruction at a time.
    • Just-In-Time (JIT) Compiler: Compiles bytecode into native machine code at runtime for better performance.
    • Garbage Collector: Manages memory by reclaiming memory used by objects that are no longer referenced.
JVM Class Loader Method Area Heap Java Stacks PC Registers Native Method Stack Execution Engine Interpreter JIT Compiler Garbage Collector



public class ComplexExample {
    static int staticCounter = 0;
    int instanceCounter = 0;

    public ComplexExample() {
        staticCounter++;
        instanceCounter++;
    }

    public void displayCounter() {
        System.out.println("Instance Counter: " + instanceCounter);
    }

    public void displayCounter(String message) {
        System.out.println(message + ": " + instanceCounter);
    }

    public static void displayStaticCounter() {
        System.out.println("Static Counter: " + staticCounter);
    }

    public static void main(String[] args) {
        ComplexExample obj1 = new ComplexExample(1);
        ComplexExample obj2 = new ComplexExample(2);

        obj1.displayCounter();
        obj2.displayCounter("Message for obj2");

        ComplexExample.displayStaticCounter();

        // Suggest garbage collection
        obj1 = null;
        obj2 = null;
        System.gc();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Garbage collection suggested.");
    }
}
    

When the program is run, the JVM performs the following steps:

  • Class Loading: The JVM loads the ComplexExample.class file into the Method Area (Metaspace).
  • Object Creation: Instances obj1 and obj2 are created in the Eden space of the Young Generation.
  • Static Field Initialization: staticCounter is initialized and shared among all instances.
  • Method Invocation: Various methods are called, demonstrating the use of static and instance fields.
JVM Execution Flow Class Loading Method Area Object Creation in Heap Static Field Initialization Method Invocation Garbage Collection End of Execution Class Loader loads ComplexExample.class Stores class-level data (static fields, methods) Heap: Allocates memory for obj1 and obj2 Eden space in Young Generation staticCounter initialized and shared Method Calls: displayCounter() and displayStaticCounter() Operand Stack and Local Variables used Objects obj1 and obj2 set to null System.gc() suggests garbage collection Finalization and resource release

Memory in the JVM is divided into several areas:

  • Method Area (Metaspace): Stores class-level data including static fields.
  • Heap: Allocates memory for object instances and instance variables.
  • Java Stacks: Each thread has its own stack containing frames for method calls, local variables, and operand stacks.
Memory Management in JVM Class Loader Memory Stores class metadata Loaded classes info Method Area Stores class structures Static variables, methods Heap Stores all objects Instance variables Stack Memory Stores method calls Local variables PC Register Contains the address of the current instruction Native Method Stack Contains native method information Execution Engine Interpreter Interprets bytecode JIT Compiler Compiles to native code Garbage Collector Manages memory

Garbage Collection (GC) is the process of automatically identifying and reclaiming memory that is no longer in use. The JVM uses a generational approach, dividing the heap into the Young Generation and Old Generation.

  • Young Generation: Where new objects are created. It consists of the Eden space and two Survivor spaces.
  • Old Generation: Where long-lived objects are moved after surviving multiple GC cycles.

GC involves marking reachable objects, sweeping and reclaiming memory of unreachable objects, and optionally compacting memory to reduce fragmentation.

Garbage Collection in JVM Young Generation Eden Space New objects allocated here Survivor S0 Survivor S1 Old Generation Long-lived objects moved here Permanent Generation Metadata, classes, methods Garbage Collection Phases Mark Sweep Compact

In the example program, garbage collection can be suggested using System.gc(). This makes obj1 and obj2 eligible for GC by setting them to null. Note that System.gc() is only a request and not a guarantee that GC will occur immediately.


obj1 = null;
obj2 = null;
System.gc();
    

This request is followed by a short delay to allow time for GC to run.


  1. Monitoring garbage collection with jstat
  2. Use jstat -gc $JAVA_PID to monitor gc
    Monitoring garbage collection with jstat
  3. Monitoring garbage collection with the jconsole user interface
  4. 1) Run the Java Program provided in the git repository, which causes jvm to go outofmemory.
    https://github.com/deonash/techoral-git
    2) Use jconsole command to launch Jconsole Interface. Select running java process to monitor the heap usage.
    Monitoring garbage collection with the jconsole user interface

    after few minutes, above program fails with OutOfMemoryError as shown below.
    jvm fails with outofmemoryerror



How Launch Jconsole - Quick Tip :


Watch Video on YouTube

The JVM is a powerful component of the Java platform, enabling efficient execution and memory management of Java programs. Understanding how the JVM manages memory and performs garbage collection helps developers write more efficient and optimized code. Using techniques like requesting GC can help manage memory, but it should be done with an understanding of its non-deterministic nature.