Samet Nangshe, Phang Nga, Thailand

Java Memory Model: A Comprehensive Guide

The Java Trail

--

Java Memory Model

The Java Memory Model (JMM) governs how threads in Java interact with memory. It guarantees that changes made by one thread become visible to others, providing a framework for safe multi-threading. JMM ensures proper synchronization through constructs like `synchronized` blocks, `volatile` variables, and memory barriers. It’s crucial for preventing data races and ensuring consistent behavior in multi-threaded Java programs. Understanding JMM is fundamental for writing reliable and efficient concurrent code.

Thread Stacks

Each thread running within the Java virtual machine has its own thread stack. Local variables for primitive types are fully stored on the thread stack and are not visible to other threads. Even if two threads are executing the same code, they will create their own separate copies of local variables for that code in their respective thread stacks

How Thread Stack is created?

  1. When the thread is started, the Java runtime environment creates a new thread stack for the thread. The thread stack is initially empty.
  2. When the thread calls methodA(), the runtime environment pushes a new frame onto the thread stack. The frame contains the call stack and local variables for methodA().
  3. When methodA() calls methodB(), the runtime environment pushes another frame onto the thread stack. The frame contains the call stack and local variables for methodB().
  4. When methodB() returns, the runtime environment pops the frame for methodB() off the thread stack. When methodA() returns, the runtime environment pops the frame for methodA() off the thread stack.

Key Features of Stack Memory

  1. Dynamic Growth and Shrinkage: Stack memory grows and shrinks as new methods are called and returned, respectively. When a method is called, a new stack frame is created, and when the method returns, the corresponding stack frame is removed.

2. Limited Lifetime: Variables declared in the stack exist only as long as the method that created them is running. Once the method finishes execution, the stack frame and its local variables are automatically deallocated.

3. Automatic Allocation and Deallocation: Stack memory is automatically allocated when a method is called and deallocated when the method finishes executing. You don’t need to manage memory manually.

4. StackOverflowError: If the stack memory becomes full due to excessive method calls (e.g., recursion without a base case), Java throws a `StackOverflowError`. This error indicates that the stack has reached its limit.

public class StackOverflowExample {
public static void main(String[] args) {
recursiveMethod();
}

public static void recursiveMethod() {
recursiveMethod(); // Recursive call without a base case.
}
}

5. Fast Access: Stack memory offers fast access compared to heap memory because it operates with a simple Last-In, First-Out (LIFO) mechanism. Accessing variables on the stack is efficient.

6. Thread Safety: Stack memory is inherently thread-safe because each thread operates in its own stack. Each thread’s method calls and local variables are isolated from other threads.

Importance of Thread Stacks

They allow threads to execute independently of each other and to have their own local variables. This is essential for multithreaded programming.

==========================================

The Heap

  • The heap is a separate memory area that contains all objects created in a Java application, regardless of which thread created them.
  • This includes objects of primitive types (e.g., Integer, Long).
  • Whether an object was created and assigned to a local variable or created as a member variable of another object, it is stored on the heap.

Key Features of Java Heap Memory

** OutOfMemoryError on Heap Space Full:

  • When heap space becomes insufficient to allocate new objects, Java throws an OutOfMemoryError. This error indicates that the JVM has exhausted available heap memory.
  • In real-world applications, you might encounter this error when, for example, loading a large dataset into memory or when there’s a memory leak.
public class OutOfMemoryExample {
public static void main(String[] args) {
try {
// Attempt to create a very large array to fill up heap space
int[] bigArray = new int[Integer.MAX_VALUE];
} catch (OutOfMemoryError e) {
System.err.println("OutOfMemoryError: " + e.getMessage());
}
}
}

Heap Memory Access is Slower Than Stack Memory:

  • Heap memory access is generally slower compared to stack memory because it involves dynamic memory allocation and deallocation managed by the Garbage Collector (GC).
  • Stack memory access is faster because it simply involves pushing and popping elements from the stack, which is a simpler and faster operation.

**Garbage Collector for Memory Cleanup:

  • In the heap, memory isn’t automatically deallocated when an object is no longer in use. This can lead to memory leaks if not managed properly.
  • Java relies on the Garbage Collector (GC) to identify and reclaim memory occupied by unreachable objects.

**Heap Memory is Not Thread-Safe:

  • Heap memory is not inherently thread-safe, meaning that when multiple threads access and modify shared objects in the heap concurrently, it can lead to data races and inconsistent states.
  • Proper synchronization mechanisms like synchronized blocks, locks, or using thread-safe data structures from the java.util.concurrent package should be used to make heap-based data structures thread-safe.

==========================================

Local Variables and Objects

Local Variables

- Local variables in Java can be of primitive types or references to objects.
- Primitive-type local variables (e.g., `int`, `double`) are entirely stored on the thread stack and are not shared with other threads.
- If a local variable is a reference to an object, the reference itself is stored on the thread stack, but the actual object is stored on the heap.

Objects and Their Member Variables

- Objects, whether they are created as standalone instances or as member variables of other objects, are stored on the heap.
- An object’s member variables, whether they are of primitive types or references to other objects, are also stored on the heap.
- Even if an object’s methods contain local variables, these local variables are stored on the thread stack of the executing thread, not on the heap.

Static Class Variables

- Static class variables are also stored on the heap along with the class definition.
- These variables are shared among all instances of the class and all threads that access the class.

Accessing Objects and Member Variables

- Objects on the heap can be accessed by all threads that have references to them.
- When multiple threads call methods on the same object concurrently, they can access the object’s member variables.
- Each thread has its own copy of local variables when executing methods, even when working with the same code.

Explanation of how memory is used for a Java Code:

class Person {
int id;
String name;

public Person(int id, String name) {
this.id = id;
this.name = name;
}
}

public class PersonBuilder {
private static Person buildPerson(int id, String name) {
return new Person(id, name);
}

public static void main(String[] args) {
int id = 23;
String name = "John";
Person person = null;
person = buildPerson(id, name);
}
}

Step 1: Method Invocation and Stack Memory

  • When we enter the main() method, a space in the stack memory is created to store primitives and references of this method.

Step 2: Stack Memory for Primitive (id)

  • Stack memory directly stores the primitive value of the integer id, which is 23.

Step 3: Stack Memory for Reference (person)

  • The reference variable person of type Person is created in stack memory, initialized to null. This reference will eventually point to an actual object in the heap.

Step 4: Heap Memory for Person Object

  • The call to the parameterized constructor Person(int, String) from main() allocates memory on the heap for a new Person object. This new object will have two instance variables: id and name.

Step 5: Stack Memory for Constructor Call

  • Further memory allocation takes place on top of the previous stack memory for the constructor call. This will store:
  • The this object reference of the calling Person object in stack memory.
  • The primitive value id (23) in the stack memory.
  • The reference variable of the String argument name, which will point to the actual string in the string pool in heap memory.

Step 6: Heap Memory for String Object (name)

  • The String argument name is a reference to the actual string "John" from the string pool in heap memory. The string "John" is created in the heap memory.

Step 7: Stack Memory for buildPerson() Method Call

  • The main method calls the buildPerson() static method, and further allocation takes place in stack memory on top of the previous one. This will again store variables in the manner described above, including the creation of a new Person object on the heap.

Step 8: Heap Memory for Another Person Object

  • The buildPerson() method creates a new Person object on the heap, just like in the main method. This new object also has its own set of id and name instance variables.

Memory Summary:

Stack Memory:

  • id (int): 23
  • person (Person reference): Initially null, later points to a Person object in the heap.

Heap Memory:

  • Two Person objects, each with its own set of id and name instance variables.

--

--

The Java Trail

Scalable Distributed System, Backend Performance Optimization, Java Enthusiast. (mazumder.dip.auvi@gmail.com Or, +8801741240520)