Sitemap

The Untold Story of Java Memory Management

10 min readAug 24, 2025

--

Press enter or click to view image in full size
Cartoon of the Java mascot angrily pouring coffee into a worried memory box labeled “MEMORY.”

Memory Management is the most important topic in Java that every developer should understand, the stuff you may have thought to ignore (but can’t).

Guys, think about an e-commerce website like Amazon. During a big sale (like Black Friday), thousands of users add items to their cart. If memory isn’t managed properly. For example, if every cart object stays in memory even after checkout or logout, the server can quickly run out of memory. The result? Slow responses, system crashes, and lost sales.

When writing Java code, it’s easy to assume the Garbage Collector will handle everything for you since it automatically frees unused memory.

Bad news: memory isn’t infinite. Once an application exceeds available memory, Java throws an OutOfMemoryError, a critical error that can abruptly terminate your program.

If you want your code to survive in production (and not die during a client demo), you need to understand how Java manages memory.

Let’s dive deep without drowning in jargon,

Press enter or click to view image in full size

In Java, memory is managed by the JVM (Java Virtual Machine), which acts as the runtime environment that executes Java bytecode.

Before diving deep into JVM internals, let’s first understand how values and references are stored.
For now, think of memory being split into two main areas: Stack and Heap.

Press enter or click to view image in full size

In Java, all variables and primitive values are stored directly in the stack. For objects, only the reference variable is stored in the stack, while the actual object is stored in the heap. Moreover, many reference variables can point to the same object. If the object value is changed, the change is visible through all reference variables.

List<Integer> a = new ArrayList<>(Arrays.asList(2, 1, 3, 4));
List<Integer> b = a; // both 'a' and 'b' refer to the same object
List<Integer> c = new ArrayList<>(Arrays.asList(0, 8, 9, 5)); // new object
Press enter or click to view image in full size

If there is an object in the heap that points to no reference variable, then it is garbage collected.

In contrast, in C/C++, all variables and objects are stored in the stack unless we use malloc or new to specifically allocate memory on the heap.

Now, let’s dive deep into how primitive datatypes are stored in the memory,

As we know, primitive data types are the basic building blocks for handling data efficiently. They take up less memory and are processed faster because they store raw values directly in the stack, unlike objects, which are stored in the heap.

Java has 8 primitive types,

// Integer types
byte b = 10; // 1 byte (8 bits) Range: -128 to 127
short s = 1000; // 2 bytes (16 bits) Range: -32,768 to 32,767
int i = 50000; // 4 bytes (32 bits) Range: -2^31 to 2^31 - 1
long l = 100000L; // 8 bytes (64 bits) Range: -2^63 to 2^63 - 1

// Floating point types
float f = 3.14f; // 4 bytes (32 bits) Precision: ~6–7 decimal digits
double d = 3.14159; // 8 bytes (64 bits) Precision: ~15 decimal digits

// Character type
char c = 'A'; // 2 bytes (16 bits) Range: 0 to 65,535 (Unicode)

// Boolean type
boolean flag = true; // JVM dependent (usually 1 bit, stored as 1 byte)

Local primitive data types live in the stack; if they are part of an object or a class, it is stored in the heap.

Press enter or click to view image in full size

Since every primitive type is predefined, Java knows exactly how much space to reserve for them in memory. They occupy the same amount of memory regardless of the system architecture.

For boolean, it could be stored in just 1 bit, but Java does not store it that way. Instead, each boolean is stored as 1 full byte (8 bits). This is because the CPU reads memory in bytes, not bits.

Using bytes avoids something called word tearing, which happens when two or more threads try to change values stored in the same bit.

Basically, CPUs prefer bytes; it’s easier for them to access whole bytes rather than figuring out which bit inside a byte belongs to a variable.

Now let’s take a function as an example,

public class PrimitiveExample {

public static void main(String[] args) {
int num = 10;
System.out.println("Before method call: " + num);

changeValue(num);

System.out.println("After method call: " + num);
}

public static void changeValue(int x) {
int sum = x + 5;
System.out.println("Inside method: " + sum);
}
}
Before method call: 10
Inside method: 15
After method call: 10

In Java, when we pass a primitive value to a function, it is always pass by value. This means that only a copy of the variable’s value is sent to the function, not the original one. When the function is called, the JVM creates a new block of memory in the stack, called a stack frame, to hold that function’s local variables and parameters.

Press enter or click to view image in full size

The stack itself follows the LIFO (Last In, First Out) rule, so the most recently called function is the first one removed once it finishes execution.

In the above, we have a variable num with the value 10 and call changeValue(num), the number 10 is copied into the parameter x. Inside the changeValue function, sum is updated to 15, but this change only affects the copy inside the function’s stack frame. The original variable num in the main method remains 10.

Once the function completes, its stack frame is discarded automatically by the JVM. Since stack memory is automatically managed, there is no need for garbage collection in this case.

Now let’s deep dive into heap,

In heap memory, objects are allocated dynamically at runtime and require garbage collection.

Integer num = new Integer(1);

When we declare an Integer object, the JVM stores it in the heap, along with some extra information inside the object header:

  • Class Metadata: The JVM keeps a small piece of data inside the object header to remember its class type.
  • Hash Code: Objects in Java can be used in HashMaps, HashSets, etc. For that, they often need a hash code, which is a number derived from the object’s data. This hash code is stored in the object header, so it doesn’t need to be recalculated every time. (Mainly, HashMaps don’t use the reference variable directly; they use the hash code.)
  • Padding — If the object data doesn’t perfectly fit in memory, the JVM adds a little space called padding to make it line up properly.

In general, when we need an object, the JVM first reads the reference from the stack to find the actual object in the heap. This is slower than accessing stack memory directly because the heap is larger and accessed indirectly.

Why stack is faster: Stack memory is small and frequently used, so the values stored in the stack have a higher chance of being kept in the CPU cache, making access faster.

Now that you have an idea of how primitive values and objects are stored, let’s dig deeper into JVM memory.

The JVM memory has 5 main areas and looks like this:

Press enter or click to view image in full size

Method Area:

The method area is one of the most interesting areas in memory, at least to me.

  • It stores class data such as class variables, the constant pool, class structures, method bytecode, and interfaces.
  • Previously, in Java versions before 8, it was called PermGen (Permanent Generation), was part of the heap, and was not garbage collected.
  • In Java 8 and later, it is called Metaspace, is part of native memory, and is garbage collected when classes are unloaded.
  • Each method area is created per JVM and is shared by all threads.

Now let’s see what method area stores in depth,

1. Class Variables

Class variables are declared with the static keyword in a class.

class Student {
static int count = 0;
String name;
}

count lives in the method area, and only one copy exists for the class and name is stored in the heap for each object separately.

2. Constant Pool

The constant pool stores constants and reusable literals. Instead of creating multiple copies of the same literal, Java keeps a single copy in the constant pool and reuses it.

There are two types of constant pools:

(a) String Constant Pool

A special memory area inside the heap for storing string objects.

String s1 = "Hello";
String s2 = "Hello";

Here both s1 and s2 points to the same literal in the String Constant Pool.

If we create a string using new String("Hello"), a new object is created outside the string constant pool.

(b) Runtime Constant Pool

Get Harish Raghav’s stories in your inbox

Join Medium for free to get updates from this writer.

Stores string literals, primitive constants, and symbolic references (method names, class names, field names) used in a class. Each class file has its own runtime constant pool, and the JVM uses it when the class is loaded.

(i) Primitive constant

public class Demo {
int x = 10; // '10' is stored in the runtime constant pool
}

(ii) References to Methods

public class Demo {
public static void main(String[] args) {
String s = "Hello";
int len = s.length();
}
}

The method length() is not hardcoded in machine language. The runtime constant pool stores a reference after compilation.

Method java/lang/String.length:()I

This means: Method length of class Stringtakes no parameters (), returns an int I

JVM resolves this reference at runtime using dynamic linking.

(iii) Reference to Classes

public class Demo {
String s = "Hello";
}

The runtime constant pool stores the class reference as,

class java/lang/String

Here"Hello"is stored in the string constant pool. If the same literal is present in the pool, Java reuses it instead of creating a new object, which helps save memory and improves performance.

Note: At compile-time, no objects are created in Java; object creation happens only at runtime.

(iv) Reference to Fields

class Person {
String name = "Hello";
}

public class Demo {
public static void main(String[] args) {
Person p = new Person();
System.out.println(p.name);
}
}

The constant pool will contain:

Field Person.name:Ljava/lang/String;

This means: In class Person, the field name is of type String.

3. Class Structures

  • Contains the class name (e.g., Student)
  • Parent class (e.g., Object)
  • Implemented interfaces
  • Method names and their return types
  • Field names and types (both static and non-static)
  • Access modifiers (public, private, protected)
  • Constant pool references

4. Method ByteCode

Every method written in Java is compiled into bytecode

5. Interfaces

Stores the interface names and method signatures of each interface

Heap Area:

  • In Java, the heap is where objects, their instances, and primitive arrays (like String[], int[]) are stored.
  • Any object created using new is stored in the heap, which is the only way to create objects and arrays.
  • Garbage Collector (GC) automatically reclaims memory occupied by objects that are no longer reachable from any thread.
  • Each JVM has its own heap.
  • Thread Safety: Objects stored in the heap can be accessed by multiple threads simultaneously. This can lead to race conditions, so extra care, such as using synchronized blocks or locks, is needed to manage concurrent access safely.

Stack Area:

  • In Java, the stack stores method execution data, including local variables, return addresses, and method arguments.
  • Each thread has its own stack, and whenever a method is called, a stack frame is created for it within the thread’s stack.
  • Each stack frame contains three main parts,
  • Local Variable Area (LVA), which holds the method’s local variables and parameters.
  • Operand Stack (OS), which temporarily stores operands and computation results during method execution.
  • Frame Data (FD), which keeps metadata such as the return address and other housekeeping information.
  • When the method finishes execution, its stack frame is removed following the LIFO (Last In, First Out) principle, ensuring efficient management of method calls and local data.
  • Thread safety: Primitive variables stored in the stack are automatically thread-safe, because each thread works with its own copy. No synchronization is needed.

Program Register:

  • The Program Counter (PC) register tells the JVM which instruction in the current method should execute next.
  • Every thread has its own pc register.
  • Each thread has its own PC register, which stores the address (or offset) of the next JVM bytecode instruction that the thread needs to execute.
  • For native methods, the value of the PC register is undefined.
Press enter or click to view image in full size

Native Memory Stack:

Each thread in Java also has its own native method stack, in addition to the JVM stack.

Normally, Java code runs inside the JVM, but sometimes we need to run code written in another language, like C or C++, often for performance reasons or to access operating system or hardware-level features.

This kind of code is called native code, and Java interacts with it using the Java Native Interface (JNI).

public class Demo {
public static void main(String[] args) {
System.loadLibrary("nativeLib"); // Loads the C++ library
nativeMethod();
}

public static native void nativeMethod(); // Declared in Java, implemented in C++
}

Here, the method nativeMethod() is declared in Java but does not have a Java implementation—the actual code is written in C or C++.

How it works internally:

  • Java code runs normally in the JVM.
  • When a native method is called, the JVM transfers execution to the native method stack.
  • Inside this native stack, C/C++ functions get their own call frames, local variables, and execution context (just like Java methods do in the JVM stack).
  • Once the native function finishes execution, control returns to the JVM stack, and Java execution continues.

Ahh, what a long ride that was! We jumped into heaps, stacks, registers, and even dragged poor C and C++ along for the show.

But here’s the twist: once you actually write and run the Java code, it all feels so simple that you wonder why you even stressed in the first place.

Classic Java, making things look harder than they are! Now go celebrate, you officially survived JVM memory land without losing your sanity.

--

--