This topic is dedicated to one of the fundamental ideas concerning the memory used by the application: we'll talk about heap. You will find out what its role is, what is stored there, and how the application data gets there.
The heap
The heap space is a special section of memory located in Random Access Memory (RAM), where the application stores objects created during the execution. It is closely related to stack since objects are referred from the heap using variables from the stack that store a reference to the object.
Let's see an example of how it works. Take a look at the following code: Object obj = new Object(). In the left part, we declared a variable of type Object that is stored in the stack. In the right part, two operations are performed. First, the new operator dynamically allocates memory space for the object which will be created and attached to the appropriate variable in the stack. Then the constructor initializes the object, that is, it assigns initial values to the object fields.
Speaking of dynamic memory allocation, it is important to understand that the developer doesn't have to think about when and how much memory to allocate for an object. This is done automatically when the application is executed.
If we assume that in our example the object was created at the address 0x001, then we will have this structure inside the memory:
Note that each object doesn't need to be directly associated with a variable from the stack. We'll talk about it in detail in the next section.
Heap structure
The heap is a region of memory created when a Java program starts. This is where all objects are stored during program execution. Unlike the stack, where data is stored sequentially and tied to thread frames, the heap supports complex object graphs — objects that reference each other in flexible, non-linear ways. That is, an object inside a heap can be single or reference other objects. And unlike the stack (which is thread-specific), the heap is shared across all threads.
Objects in the heap are managed by the Garbage Collector. The garbage collector automatically reclaims memory from objects that are no longer reachable. That is, the objects in the heap are created and automatically removed by the Garbage Collector when they're no longer reachable from any part of the program. This automatic memory management is one of Java's key features.
In the previous section, we mentioned that an object is not always directly connected to a stack variable. Here is an example of a situation where you will see the internal structure of the heap:
In the image above, only object 0x001 is associated with the obj_1 variable. The other objects are connected to it via object 0x001. On the other hand, object 0x005 was associated with the variable obj_2, but at some point in the execution of the program, their relationship was removed. Since nothing else points to 0x005, it becomes eligible for garbage collection by the JVM. That is, now this object is unused and available for deletion by the JVM.
Metaspace and PermGen
Until Java 7, a heap space had a special separate section called a Permanent Generation (PermGen) which stored loaded class metadata, String Pool objects, and some other stuff. However, it was prone to memory issues and difficult to size correctly.
Starting with Java 8, PermGen was removed and replaced with Metaspace, which isn't part of heap and so, stores class metadata outside the heap. Metaspace can increase its size automatically (i.e. can grow dynamically), based on the needs of the application
Object structure in the Heap
Each Java object residing in the heap typically consists of:
Object header: This segment stores crucial metadata about the object. It generally includes:
Mark word: This is a multi-purpose word that holds information such as the object's identity hash code (when calculated), locking information (used for
synchronizedblocks and methods, including biased locking, lightweight locking, and heavyweight locking states), and garbage collection flags. The specific contents and size of the mark word can vary slightly between JVM implementations and versions.Klass pointer (or class pointer): This is a reference (a pointer) to the object's class metadata. This pointer allows the JVM to know which class the object belongs to, enabling it to access the class's methods, fields, and other static information. In HotSpot JVM, this often points to an internal
_klassstructure.
Instance fields (or instance data): This section contains the actual data associated with the object's instance variables (non-static fields). These fields are laid out in memory according to specific rules, which include:
Declaration order: Generally, fields declared earlier in the class definition tend to be laid out before fields declared later, though the JVM can optimize this.
Alignment rules: Fields are aligned in memory to their natural size (e.g., a
longmight be aligned to an 8-byte boundary). This can sometimes lead to small gaps between fields.JVM optimizations: The JVM may reorder fields for better memory packing and alignment to reduce the overall object size, particularly when using options like
UseCompactOops(compressed ordinary object pointers). Primitives are typically grouped together, followed by object references.
Padding (if any): This refers to the extra memory added by the JVM to ensure that the total object size (header + instance fields) is a multiple of a certain word size (e.g., 8 bytes on a 64-bit system). This alignment is crucial for performance, as modern CPUs are optimized to read data in word-sized chunks. Without proper padding, reading an object could require multiple, less efficient memory accesses.
Factors influencing object structure
The precise object structure in the heap can indeed vary significantly based on several factors:
JVM implementation: Different JVMs (e.g., Oracle HotSpot, OpenJ9, Azul Zing) might have their own specific internal representations.
JVM options:
UseCompressedOops(Compressed Ordinary Object Pointers): On 64-bit JVMs, by default, object references (pointers) are compressed to 32 bits, saving significant memory, especially for large heaps. This changes how the Klass Pointer and object references within instance fields are stored.ObjectAlignmentInBytes: This option controls the byte alignment of objects in the heap (e.g., aligning all objects to 8-byte boundaries).FieldsAllocationStyle: This internal HotSpot option (though not commonly tuned directly by users) can affect how fields are packed.
Platform (32-bit vs. 64-bit):
64-bit JVMs: Without compressed oops, object pointers (including the Klass Pointer) would be 8 bytes. With compressed oops, they are 4 bytes. The overall object size tends to be larger due to larger pointers and alignment requirements.
32-bit JVMs: Object pointers are 4 bytes. The overall object size is generally smaller.
Compact Object Headers
In Java 24, JEP 450 introduced Compact Object Headers to reduce memory overhead for every object. On 64-bit platforms, it minimizes the object header size to 8 bytes (64 bits), offering these benefits:
Lower heap usage: More efficient memory layout for object-heavy workloads.
Improved cache utilization: Denser packing of objects in memory.
Reduced GC overhead: Smaller objects mean less work during collection.
To enable this experimental feature (note that experimental features may evolve or be removed in future releases), launch the JVM with:
-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeadersYou can analyze object layouts with tools like JOL (Java Object Layout).
CDS Archives with Compact Object Headers
CDS (Class Data Sharing) is a JVM feature that reduces startup time by storing preprocessed class metadata in a shared archive file, typically named classes.jsa.
However, if you enable Compact Object Headers (a new Java 24 feature), you won't be able to use the default CDS archive that comes with the Oracle JDK, as it doesn't support this experimental feature. Instead, you'll need to create a custom CDS archive that is compatible with Compact Object Headers.
To generate a compatible archive, use the following command:
java -Xshare:dump -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeadersThis will produce a new archive named classes_coh.jsa, usually located in the same directory as the default archive (e.g., <jdk>/lib/server).
If you also want to disable compressed oops (compressed ordinary object pointers) while using Compact Object Headers, use this command instead:
java -Xshare:dump -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders -XX:-UseCompressedOopsThis generates an archive named classes_nocoops_coh.jsa, stored alongside other CDS archives.
These custom archives preserve the startup performance benefits of CDS even when you're using Compact Object Headers to reduce memory usage.
Heap settings
Of course, memory management in Java or Kotlin is done automatically, but the HotSpot JVM provides us with some settings for heap management. Here are just a few of them:
-Xmsfor setting the initial heap size when the JVM starts. The full structure for this command is-Xms+size. For instance, if you want to set it equal to 10MB the full command is-Xms10mor-Xms10240k.-Xmxfor setting the maximum heap size. It is configured in the same way as the previous command. If you want to set it to 1GB the full command is-Xmx1gor-Xmx1024m.-XX:PermSizefor setting the initial size of the Permanent Generation. The full command for setting this option equal to 10MB looks like this: -XX:PermSize=10mor-XX:PermSize=10240k.-XX:MaxPermSizefor setting the maximum size of the Permanent Generation. If you want to set it to 1GB the full command will be -XX:MaxPermSize=1gor if you use megabytes unit it will be-XX:MaxPermSize=1024m.-XX:MetaspaceSizefor setting the initial allocated size of Metaspace. For example,-XX:MetaspaceSize=10m.-XX:MaxMetaspaceSizefor setting the maximum allowed size of Metaspace. For example,-XX:MaxMetaspaceSize=1g.
Note: The older -XX:PermSize and -XX:MaxPermSize flags for Permanent Generation are obsolete and have no effect in Java 8 and later JVMs, as PermGen was removed.
It is preferable to limit the maximum size of the heap. This will help reduce the impact of memory leaks on other processes in the operating system. Otherwise, the OS memory may be overused due to the work of your application and other processes will also lack memory.
Memory leaks occur when, due to incorrect memory management, unnecessary objects aren't deleted or become unreachable for some reason. In such situations, the memory allocated for the application is consumed faster. When the heap runs out of free memory and there isn't enough space to allocate new objects, the application will throw the OutOfMemoryError.
In this section, we mentioned just a few basic settings you can use to manage the heap size in your application. JVM has dozens of them, each providing a specific setting option for other purposes.
Conclusion
In this topic, you learned some essential information about heap space and its management options. Understanding the process of memory management is a key skill for every Java or Kotlin programmer: during their work, developers often need to optimize the application performance. That's why it's so important to know how the heap memory operates.