Contents

Memory Allocation

Java Memory Management

Java handles memory management automatically, with the help of the Java Virtual Machine (JVM) and the Garbage Collector. But it’s essential for a programmer to understand how memory management works in Java, as it aids in writing efficient code and debugging potential memory issues. Knowing how to manage memory can also help improve performance and prevent memory leaks.

Why Learn Java Memory Management?

Even though Java automates memory management through the garbage collector, the programmer’s role isn’t eliminated. While developers don’t need to explicitly destroy objects like in languages such as C/C++, they must understand how Java memory management works. Mismanaging memory or not understanding what is managed by the JVM and what isn’t can lead to issues, such as objects not being eligible for garbage collection. In particular, understanding memory management enables writing high-performance programs that avoid memory crashes and helps debug memory issues effectively.

Introduction to Java Memory Management

Memory is a vital and limited resource in any programming language. Proper memory management ensures there are no memory leaks, improving the efficiency of programs. Unlike languages like C, where the programmer directly manages memory, Java delegates memory management to the JVM, which handles allocation and deallocation of memory. The Garbage Collector plays a significant role in managing memory automatically in Java.

Key Concepts in Java Memory Management

1. JVM Memory Structure
2. Garbage Collection Process

Java Memory Structure

The JVM manages different runtime data areas, some of which are created when the JVM starts and some by threads used in a program. These memory areas have distinct purposes and are destroyed when the JVM or the respective threads exit.

Key Components of JVM Memory:

1. Heap : The heap is a shared runtime data area used for storing objects and array instances. It is created when the JVM starts. The size of the heap can be fixed or dynamic, depending on system configuration, and can be controlled by the programmer. For instance, when using the new keyword, the object is allocated space in the heap, while its reference is stored in the stack.

Example:

				
					List<String> list = new ArrayList<>();

				
			

In this case, the ArrayList object is created in the heap, and the reference list is stored in the stack.

Output:

				
					Memory allocated for ArrayList in the heap.

				
			

2. Method Area : The method area is a logical part of the heap and holds class structures, method data, and field data. It stores runtime constant pool information as well. Although it’s part of the heap, garbage collection in the method area is not guaranteed.

3. JVM Stacks : Each thread in a Java program has its own stack, which stores data like local variables, method calls, and return values. The stack is created when a thread is instantiated and destroyed when the thread finishes.

Example:

				
					public static void main(String[] args) {
    int x = 5;
    int y = calculate(x);
}

static int calculate(int val) {
    return val * 2;
}

				
			

In this example, the local variables x and y are stored in the stack. The method call to calculate is also stored on the stack.

1. Native Method Stacks: These stacks support native methods (non-Java methods). Like JVM stacks, they are created for each thread and can be either dynamic or fixed.
2. Program Counter (PC) Register : Each thread in the JVM has a program counter register that tracks the current method instruction being executed. For native methods, the value is undefined.

How the Garbage Collector Works

Java’s garbage collection is an automatic process that identifies and reclaims memory from objects that are no longer in use. It frees the programmer from manually managing memory deallocation. However, the garbage collection process can be costly, as it pauses other threads during execution. To improve performance, Java employs various garbage collection algorithms, a process referred to as “Garbage Collector Tuning.”

Garbage Collection Algorithms:

1. Generational Garbage Collection:
Java uses a generational garbage collection approach, where objects are classified based on their lifespan (age). Objects that survive multiple garbage collection cycles are promoted to an older generation, while newly created objects are placed in a younger generation. This improves efficiency, as older objects are collected less frequently.

Garbage Collection Example:

				
					public class GarbageCollectionDemo {
    public static void main(String[] args) {
        GarbageCollectionDemo demo = new GarbageCollectionDemo();
        demo = null; // Eligible for garbage collection
        System.gc(); // Requesting garbage collection
        System.out.println("Garbage collection triggered.");
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Garbage collected!");
    }
}

				
			

Output:

				
					Garbage collection triggered.
Garbage collected!

				
			

Here, the object demo is made eligible for garbage collection by setting it to null. The System.gc() method requests the JVM to run the garbage collector, although it’s not guaranteed to happen immediately.

Java Object Allocation on Heap

In Java, all objects are dynamically allocated on the heap. This differs from languages like C++, where objects can be allocated on either the stack or the heap. When you use the new keyword in Java, the object is allocated on the heap, whereas in C++, objects can also be stack-allocated, unless they are declared globally or statically.

When you declare a variable of a class type in Java, memory for the object is not allocated immediately. Only a reference is created. To allocate memory to an object, the new keyword must be used. This ensures that objects are always allocated on the heap.

Creating a String Object in Java

There are two ways to create a string in Java:

1. By String Literal
2. By using the new Keyword

1. String LiteralThis is the most common way to create a string in Java, using double-quotes.

Example:

				
					System.out.println("Hello");  // valid
system.out.println("Hello");  // invalid

				
			

In this case, every time a string literal is created, the JVM checks whether the string already exists in the string constant pool. If it does, a reference to the pooled instance is returned. If it doesn’t, a new string instance is created in the pool. Therefore, only one object will be created for both str1 and str2 if they have the same value.

The JVM is not obligated to create new memory if the string already exists in the pool.

2. Using the new Keyword : You can also create strings using the new keyword.

Example:

				
					String str1 = new String("Hello");
String str2 = new String("Hello");

				
			

Here, both str1 and str2 are different objects. Even though the string content is the same, the JVM creates separate memory locations in the heap for each object when using the new keyword. The JVM will not check if the string already exists in memory; it always creates new memory for each object.

The JVM is forced to allocate new memory for each string object created using new.

Uninitialized Object Example

If you attempt to use a reference to an object without initializing it, Java will throw a compilation error, as the object does not have memory allocated to it.

Example with Error:

				
					class Demo {
    void display() {
        System.out.println("Demo::display() called");
    }
}

public class Main {
    public static void main(String[] args) {
        Demo d;  // No memory allocated yet
        d.display();  // Error: d is not initialized
    }
}

				
			

Output:

				
					Error: variable d might not have been initialized

				
			

How many types of memory areas are allocated by JVM?

The Java Virtual Machine (JVM) is an abstract machine, essentially a software program that takes Java bytecode (compiled Java code) and converts it into machine-level code that the underlying system can understand, one instruction at a time.

JVM acts as the runtime engine for Java applications and is responsible for invoking the main() method in Java programs. It is a core part of the Java Runtime Environment (JRE), which provides the necessary libraries and environment for Java code execution.

Functions of the JVM

JVM performs several key functions:

1. Loading of code: It loads the bytecode into memory.
2. Verification of code: It checks the bytecode for security issues or invalid operations.
3. Execution of code: It executes the bytecode.
4. Runtime environment: Provides a runtime environment for Java programs.

ClassLoader

ClassLoader is a crucial subsystem of the JVM that loads .class files into memory. It is responsible for:

1. Loading: Loading the class into memory.
2. Linking: Resolving symbolic references and ensuring class dependencies are loaded.
Initialization: Preparing the class for use, initializing variables, etc.

Types of Memory Areas Allocated by JVM

JVM memory is divided into several parts that perform different functions. These are:

1. Class (Method) Area : The Class Method Area is a memory region that stores class-related data, such as:

  • Class code
  • Static variables
  • Runtime constants
  • Method code (functions within classes)

This area holds data related to class-level information, including constructors and field data.

2. Heap : The Heap is where objects are dynamically created during the execution of a program. This memory area stores objects, including arrays (since arrays are objects in Java). The heap is where memory is allocated at runtime for class instances.

3. Stack : Each thread in a Java program has its own stack, which is created when the thread starts. The stack holds data like:

  • Method call frames
  • Local variables
  • Partial results

A new frame is created every time a method is called, and the frame is destroyed once the method call is completed.

4. Program Counter (PC) Register

Each thread has a Program Counter (PC) register. For non-native methods, it stores the address of the next instruction to execute. In native methods, the PC value is undefined. It also holds return addresses or native pointers in certain cases.

5. Native Method Stack

The Native Method Stack is used by threads that execute native (non-Java) code. These stacks, sometimes referred to as C stacks, store information about native methods written in other programming languages like C/C++. Each thread has its own native method stack, and it can be either fixed or dynamic in size.

JVM Example: Code Execution

Here’s an example demonstrating how the JVM manages memory:

				
					class Demo {
    static int x = 10; // Stored in the heap memory
    int y = 20; // Stored in the heap memory

    void display() {
        System.out.println("x: " + x + ", y: " + y);
    }
}

public class Main {
    public static void main(String[] args) {
        Demo obj = new Demo();  // obj created in heap
        obj.display();  // Call method using obj
    }
}

				
			

Output:

				
					x: 10, y: 20

				
			

Garbage Collection in Java

Garbage Collection (GC) in Java is the process through which Java programs handle automatic memory management. Java programs are compiled into bytecode that runs on the Java Virtual Machine (JVM). As the program runs, objects are created on the heap memory, which is dedicated to the program’s use. Over time, some of these objects are no longer needed. The garbage collector identifies these unused objects and removes them, freeing up memory space.

What is Garbage Collection?

In languages like C and C++, developers are responsible for managing both the creation and destruction of objects. However, developers often neglect the destruction of objects that are no longer required, which can lead to memory shortages and eventual program crashes, resulting in OutOfMemoryErrors.

In contrast, Java handles memory management automatically. The garbage collector in Java’s JVM frees up heap memory by destroying unreachable objects. This background process is the best example of a daemon thread, which continuously runs to manage memory.

How Does Garbage Collection Work in Java?

Java’s garbage collection is fully automatic. It inspects heap memory, identifying objects that are still in use and those that are not. In-use objects are referenced by the program, while unused objects are no longer referenced by any part of the program and can have their memory reclaimed.

The garbage collector, part of the JVM, handles this process without the programmer needing to explicitly mark objects for deletion.

Types of Garbage Collection in Java

There are typically two types of garbage collection activities:

1. Minor (Incremental) GC: This occurs when objects that are no longer referenced in the young generation of the heap are removed.
2. Major (Full) GC: This occurs when objects that have survived multiple minor collections are promoted to the old generation of the heap and are later removed when they become unreachable.

Key Concepts Related to Garbage Collection

1. Unreachable Objects: An object is considered unreachable if no references to it exist within the program. Objects that are part of an “island of isolation” are also considered unreachable. For example:

				
					Integer i = new Integer(5);
// 'i' references the new Integer object
i = null;
// The Integer object is now unreachable

				
			

2. Eligibility for Garbage Collection: An object becomes eligible for garbage collection when it becomes unreachable, such as after nullifying the reference:

				
					Integer i = new Integer(5);
i = null;  // Now, the object is eligible for GC.

				
			
How to Make an Object Eligible for Garbage Collection

While Java automatically handles garbage collection, programmers can help by making objects unreachable when they are no longer needed. Here are four ways to make an object eligible for GC:

1. Nullifying the reference variable
2. Re-assigning the reference variable
3. Using objects created inside a method
4. Islands of Isolation (when a group of objects reference each other but are no longer referenced elsewhere)

Requesting JVM to Run the Garbage Collector

Garbage collection doesn’t happen immediately when an object becomes eligible. The JVM will run the garbage collector at its discretion. However, we can request the JVM to perform garbage collection using these methods:

1. System.gc(): Invokes the garbage collector explicitly.

2. Runtime.getRuntime().gc(): The Runtime class allows an interface with the JVM, and calling its gc() method requests garbage collection.

Finalization

Before an object is destroyed, the garbage collector calls its finalize() method to allow cleanup activities (e.g., closing database connections). This method is defined in the Object class and can be overridden.

				
					protected void finalize() throws Throwable {
    // Cleanup code here
}

				
			
Important points about finalize():
  • The finalize() method is never called more than once for an object.
  • If the finalize() method throws an uncaught exception, it is ignored, and finalization terminates.
  • After the finalize() method is invoked, the garbage collector reclaims the object.
Advantages of Garbage Collection
  • Memory efficiency: Garbage collection helps reclaim memory by removing unreferenced objects from heap memory.
  • Automation: Garbage collection happens automatically as part of the JVM, removing the need for manual intervention.
Example:
				
					class Employee {
    private int ID;
    private String name;
    private int age;
    private static int nextId = 1;  // Common across all objects

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
        this.ID = nextId++;
    }

    public void show() {
        System.out.println("ID: " + ID + "\nName: " + name + "\nAge: " + age);
    }

    public void showNextId() {
        System.out.println("Next employee ID: " + nextId);
    }
}

public class Company {
    public static void main(String[] args) {
        Employee e1 = new Employee("Employee1", 30);
        Employee e2 = new Employee("Employee2", 25);
        Employee e3 = new Employee("Employee3", 40);

        e1.show();
        e2.show();
        e3.show();
        e1.showNextId();
        e2.showNextId();
        e3.showNextId();

        {  // Sub-block for interns
            Employee intern1 = new Employee("Intern1", 22);
            Employee intern2 = new Employee("Intern2", 24);
            intern1.show();
            intern2.show();
            intern1.showNextId();
            intern2.showNextId();
        }

        // X and Y are out of scope, but nextId has incremented
        e1.showNextId();  // Expected 4, but it will give 6 as output
    }
}

				
			

Output:

				
					ID: 1
Name: Employee1
Age: 30
ID: 2
Name: Employee2
Age: 25
ID: 3
Name: Employee3
Age: 40
Next employee ID: 4
Next employee ID: 4
Next employee ID: 4
ID: 4
Name: Intern1
Age: 22
ID: 5
Name: Intern2
Age: 24
Next employee ID: 6
Next employee ID: 6
Next employee ID: 6

				
			

Types of JVM Garbage Collectors in Java with implementation details

Garbage Collection: Garbage Collection (GC) is a key feature in Java that enables automatic memory management. GC is responsible for reclaiming memory used by objects that are no longer needed, making that memory available for future use. To achieve this, the garbage collector monitors objects in memory, identifies those that are still referenced, and deallocates the memory for objects that are no longer in use. One common approach used by garbage collectors is the Mark and Sweep algorithm, which marks objects that are still reachable and then sweeps away the unmarked ones to free up memory.

Types of Garbage Collection in Java

The Java Virtual Machine (JVM) provides several garbage collection strategies, each affecting the application’s throughput and the pause time experienced during collection. Throughput measures how fast the application runs, while pause time indicates the delay introduced during garbage collection.

1. Serial Garbage Collector : The Serial Garbage Collector is the simplest form of GC, using a single thread to perform garbage collection. When this collector is in use, it stops all application threads during the collection process, known as stop-the-world behavior. Since it uses only one thread, it is not ideal for multi-threaded applications or environments where responsiveness is crucial. As a result, while it reduces complexity, it increases application pause time, negatively impacting throughput. This collector is suitable for smaller applications or single-threaded systems.

Usage:
To use the Serial Garbage Collector explicitly, execute your application with the following JVM argument:

				
					java -XX:+UseSerialGC -jar MyApplication.jar

				
			

2. Parallel Garbage Collector : The Parallel Garbage Collector, also known as the Throughput Collector, is the default collector in Java 8. It improves upon the Serial Collector by using multiple threads to perform garbage collection, allowing for better throughput at the expense of longer pause times. Like the Serial Collector, it stops all application threads during the garbage collection process. However, it provides control over the number of threads the collector uses and allows you to specify maximum pause times.

Usage:
To run the Parallel Garbage Collector with a specified number of threads:

				
					java -XX:+UseParallelGC -XX:ParallelGCThreads=<num_of_threads> -jar MyApplication.jar

				
			

To limit the maximum pause time for the GC, you can set the following parameter:

				
					java -XX:+UseParallelGC -XX:MaxGCPauseMillis=<max_pause_ms> -jar MyApplication.jar

				
			

3. CMS Garbage Collector (Concurrent Mark-Sweep) : The Concurrent Mark-Sweep (CMS) Garbage Collector attempts to minimize application pauses by performing most of its work concurrently with the application. It scans memory for unreferenced objects and removes them without freezing the entire application, except in two specific scenarios:

  • When there are changes in heap memory during the garbage collection process.
  • When marking referenced objects in the old generation space.

CMS typically uses more CPU than the Parallel Collector to achieve better application responsiveness. It is ideal for applications that can afford to allocate additional CPU resources for lower pause times. To enable the CMS Garbage Collector, use the following command:

Usage:

				
					java -XX:+UseConcMarkSweepGC -jar MyApplication.jar

				
			

4. G1 Garbage Collector (Garbage-First): Introduced in Java 7 and made the default in Java 9, the G1 Garbage Collector was designed for applications that require large heap sizes (greater than 4GB). Instead of treating the heap as a monolithic memory block, G1 divides it into equal-sized regions. During garbage collection, G1 focuses on the regions with the most garbage, collecting them first, hence the name Garbage-First. Additionally, G1 compacts memory during garbage collection, reducing fragmentation and enhancing performance. This garbage collector offers significant performance benefits for larger applications, especially those running on modern JVMs.

Usage:

If you’re using a Java version earlier than 9 and want to enable the G1 Garbage Collector, specify the following JVM argument:

				
					java -XX:+UseG1GC -jar MyApplication.jar

				
			

5. Example with G1 Garbage Collector: Let’s update the example to demonstrate the use of the G1 Garbage Collector. In this scenario, we’ll manage memory for a simple Employee class.

				
					class Employee {
    private int id;
    private String name;
    private int age;
    private static int nextId = 1;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
        this.id = nextId++;
    }

    public void display() {
        System.out.println("ID: " + id + "\nName: " + name + "\nAge: " + age);
    }

    public void displayNextId() {
        System.out.println("Next employee ID will be: " + nextId);
    }

    @Override
    protected void finalize() throws Throwable {
        --nextId;
        System.out.println("Finalize called for employee ID: " + id);
    }
}

public class TestEmployee {
    public static void main(String[] args) {
        Employee emp1 = new Employee("Alice", 30);
        Employee emp2 = new Employee("Bob", 25);
        Employee emp3 = new Employee("Charlie", 35);

        emp1.display();
        emp2.display();
        emp3.display();

        emp1.displayNextId();
        emp2.displayNextId();
        emp3.displayNextId();

        {
            // Temporary employees
            Employee tempEmp1 = new Employee("David", 28);
            Employee tempEmp2 = new Employee("Eva", 22);

            tempEmp1.display();
            tempEmp2.display();

            tempEmp1.displayNextId();
            tempEmp2.displayNextId();

            // Making temp employees eligible for GC
            tempEmp1 = null;
            tempEmp2 = null;

            // Requesting garbage collection
            System.gc();
        }

        emp1.displayNextId();  // After GC, nextId should be updated correctly
    }
}

				
			

Output:

				
					ID: 1
Name: Alice
Age: 30
ID: 2
Name: Bob
Age: 25
ID: 3
Name: Charlie
Age: 35
Next employee ID will be: 4
Next employee ID will be: 4
Next employee ID will be: 4
ID: 4
Name: David
Age: 28
ID: 5
Name: Eva
Age: 22
Next employee ID will be: 6
Next employee ID will be: 6
Finalize called for employee ID: 4
Finalize called for employee ID: 5
Next employee ID will be: 4

				
			

Memory leaks in Java

In C, programmers have full control over the allocation and deallocation of dynamically created objects. If a programmer neglects to free memory for unused objects, this results in memory leaks.

In Java, automatic garbage collection helps to manage memory, but there can still be scenarios where objects remain uncollected because they are still referenced. If an application creates a large number of objects that are no longer in use but are still referenced, the garbage collector cannot reclaim their memory. These unnecessary objects are referred to as memory leaks. If the memory allocated exceeds the available limit, the program may terminate with an OutOfMemoryError. Therefore, it is crucial to ensure that objects no longer needed are made eligible for garbage collection. Tools can also help in detecting and managing memory leaks, such as:

  • HP OVO
  • HP JMeter
  • JProbe
  • IBM Tivoli

Example of a Memory Leak in Java

				
					import java.util.ArrayList;

public class MemoryLeakExample {
    public static void main(String[] args) {
        ArrayList<Object> list1 = new ArrayList<>(1000000);
        ArrayList<Object> list2 = new ArrayList<>(100000000);
        ArrayList<Object> list3 = new ArrayList<>(1000000);
        System.out.println("Memory Leak Example");
    }
}

				
			

Output:

				
					Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

				
			

Java Virtual Machine (JVM) Stack Area

In Java, when a new thread is created, the JVM assigns a separate stack for it. The memory for this stack does not need to be contiguous. The JVM performs two primary operations on this stack: it pushes and pops frames, and this stack can be referred to as the runtime stack. For each thread, every method invocation is stored in this runtime stack, which includes parameters, local variables, intermediate results, and other related data. After the method completes execution, the respective entry is removed from the stack. When all method calls finish, the stack becomes empty, and the JVM removes the empty stack just before terminating the thread. Each stack’s data is thread-specific, ensuring that the data within a thread’s stack is not accessible to other threads. Hence, local data in this stack is considered thread-safe. Each entry in the stack is known as a Stack Frame or Activation Record.

Stack Frame Structure

The stack frame consists of three key parts:

1. Local Variable Array (LVA)
2. Operand Stack (OS)
3. Frame Data (FD)

When the JVM invokes a Java method, it first checks the method’s class data to determine the required size of the local variable array and operand stack, measured in words. It then creates a stack frame of the appropriate size and pushes it onto the Java stack.

1. Local Variable Array (LVA): The local variable array is organized as a zero-based array of slots where each slot stores a 4-byte word.

  • It stores all parameters and local variables of a method.
  • Values of types int, float, and object references each occupy 1 slot (4 bytes).
  • double and long values occupy 2 consecutive slots (8 bytes total).
  • byte, short, and char values are converted to int before being stored.
  • Most JVM implementations allocate 1 slot for boolean values.

The parameters of a method are placed into the local variable array in the order they are declared. For instance, consider a method in the Example class:

				
					class Example {
    public void bike(int i, long l, float f, double d, Object o, byte b) {
        // Method body
    }
}

				
			

The local variable array for the bike() method would store each parameter in order.

2. Operand Stack (OS): The JVM uses the operand stack as a workspace for storing intermediate results from computations.

  • It is also structured as an array of words, similar to the local variable array, but the operand stack is accessed through specific instructions that push and pop values.
  • Certain instructions push values onto the operand stack, others perform operations on them, and some instructions pop the results back into the local variable array.

Example of Operand Stack Usage:

The following assembly instructions illustrate how the JVM might subtract two integers stored in local variables and store the result in another local variable:

				
					iload_0    // Push the value of local variable 0 to the operand stack
iload_1    // Push the value of local variable 1 to the operand stack
isub       // Subtract the two values on the operand stack
istore_2   // Pop the result and store it in local variable 2

				
			

3. Frame Data (FD): Frame data contains symbolic references and data related to method returns.

  • It includes a reference to the constant pool and handles normal method returns.
  • Additionally, it stores references to the exception table, which helps locate the correct catch block in case an exception is thrown during execution.

In summary, the stack frame in Java is structured to efficiently manage method calls, local variables, intermediate operations, and exception handling, providing a safe and organized environment for method execution within each thread.