Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we're going to see how the JVM lays out objects and arrays in the heap.

First, we'll start with a little bit of theory. Then, we'll explore the different object and array memory layouts in different circumstances.

Usually, the memory layout of run-time data areas is not part of the JVM specification and is left to the discretion of the implementor. Therefore, each JVM implementation may have a different strategy to layout objects and arrays in memory. In this tutorial, we're focusing on one specific JVM implementation: the HotSpot JVM.

We also may use the JVM and HotSpot JVM terms interchangeably.

2. Ordinary Object Pointers (OOPs)

The HotSpot JVM uses a data structure called Ordinary Object Pointers (OOPS) to represent pointers to objects. All pointers (both objects and arrays) in the JVM are based on a special data structure called oopDescEach oopDesc describes the pointer with the following information:

The mark word describes the object header. The HotSpot JVM uses this word to store identity hashcode, biased locking pattern, locking information, and GC metadata. 

Moreover, the mark word state only contains a uintptr_ttherefore, its size varies between 4 and 8 bytes in 32-bit and 64-bit architectures, respectively. Also, the mark word for biased and normal objects are different. However, we'll only consider normal objects as Java 15 is going to deprecate biased locking.

Additionally, the klass word encapsulates the language-level class information such as class name, its modifiers, superclass info, and so on.

For normal objects in Java, represented as instanceOop, the object header consists of mark and klass words plus possible alignment paddings. After the object header, there may be zero or more references to instance fields. So, that's at least 16 bytes in 64-bit architectures because of 8 bytes of the mark, 4 bytes of klass, and another 4 bytes for padding.

For arrays, represented as arrayOopthe object header contains a 4-byte array length in addition to mark, klass, and paddings. Again, that would be at least 16 bytes because of 8 bytes of the mark, 4 bytes of klass, and another 4 bytes for the array length.

Now that we know enough about theory, let's see how memory layout works in practice.

3. Setting Up JOL

To inspect the memory layout of objects in the JVM, we're going to use the Java Object Layout (JOL) quite extensively. Therefore, we need to add the jol-core dependency:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

4. Memory Layout Examples

Let's start by looking at the general VM details:

System.out.println(VM.current().details());

This will print:

# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

This means that the references take 4 bytes, booleans and bytes take 1 byte, shorts and chars take 2 bytes, ints and floats take 4 bytes, and finally, longs and doubles take 8 bytes. Interestingly, they consume the same amount of memory if we use them as array elements.

Also, if we disable compressed references via -XX:-UseCompressedOops, only the reference size changes to 8 bytes:

# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

4.1. Basic

Let's consider a SimpleInt class:

public class SimpleInt {
    private int state;
}

If we print its class layout:

System.out.println(ClassLayout.parseClass(SimpleInt.class).toPrintable());

We would see something like:

SimpleInt object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int SimpleInt.state                           N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

As shown above, the object header is 12 bytes, including 8 bytes of the mark and 4 bytes of klass. After that, we have 4 bytes for the int state. In total, any object from this class would consume 16 bytes.

Also, there is no value for the object header and the state because we're parsing a class layout, not an instance layout.

4.2. Identity Hash Code

The hashCode() is one of the common methods for all Java objects. When we don't declare a hashCode() method for a class, Java will use the identity hash code for it. 

The identity hash code won't change for an object during its lifetime. Therefore, the HotSpot JVM stores this value in the mark word once it's computed.

Let's see the memory layout for an object instance:

SimpleInt instance = new SimpleInt();
System.out.println(ClassLayout.parseInstance(instance).toPrintable());

The HotSpot JVM computes the identity hash code lazily:

SimpleInt object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           01 00 00 00 (00000001 00000000 00000000 00000000) (1) # mark
      4     4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0) # mark
      8     4        (object header)           9b 1b 01 f8 (10011011 00011011 00000001 11111000) (-134145125) # klass
     12     4    int SimpleInt.state           0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

As shown above, the mark word currently doesn't seem to store anything significant yet.

However, this will change if we call the System.identityHashCode() or even Object.hashCode() on the object instance:

System.out.println("The identity hash code is " + System.identityHashCode(instance));
System.out.println(ClassLayout.parseInstance(instance).toPrintable());

Now, we can spot the identity hash code as part of the mark word:

The identity hash code is 1702146597
SimpleInt object internals:
 OFFSET  SIZE   TYPE DESCRIPTION               VALUE
      0     4        (object header)           01 25 b2 74 (00000001 00100101 10110010 01110100) (1957831937)
      4     4        (object header)           65 00 00 00 (01100101 00000000 00000000 00000000) (101)
      8     4        (object header)           9b 1b 01 f8 (10011011 00011011 00000001 11111000) (-134145125)
     12     4    int SimpleInt.state           0

The HotSpot JVM stores the identity hashcode as “25 b2 74 65” in the mark word. The most significant byte is 65 since the JVM stores that value in little-endian format. Therefore, to recover the hash code value in decimal (1702146597), we have to read the “25 b2 74 65” byte sequence in reverse order:

65 74 b2 25 = 01100101 01110100 10110010 00100101 = 1702146597

4.3. Alignment

By default, the JVM adds enough padding to the object to make its size a multiple of 8.

For instance, consider the SimpleLong class:

public class SimpleLong {
    private long state;
}

If we parse the class layout:

System.out.println(ClassLayout.parseClass(SimpleLong.class).toPrintable());

Then JOL will print the memory layout:

SimpleLong object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (alignment/padding gap)                  
     16     8   long SimpleLong.state                          N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

As shown above, the object header and the long state consume 20 bytes in total. To make this size a multiple of 8 bytes, the JVM adds 4 bytes of padding.

We can also change the default alignment size via the -XX:ObjectAlignmentInBytes tuning flag. For instance, for the same class, the memory layout with -XX:ObjectAlignmentInBytes=16 would be:

SimpleLong object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (alignment/padding gap)                  
     16     8   long SimpleLong.state                          N/A
     24     8        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 8 bytes external = 12 bytes total

The object header and the long variable still consume 20 bytes in total. So, we should add 12 more bytes to make it a multiple of 16.

As shown above, it adds 4 internal padding bytes to start the long variable at offset 16 (enabling more aligned access). Then It adds the remaining 8 bytes after the long variable.

4.4. Field Packing

When a class has multiple fields, the JVM may distribute those fields in such a way as to minimize padding waste. For example, consider the FieldsArrangement class:

public class FieldsArrangement {
    private boolean first;
    private char second;
    private double third;
    private int fourth;
    private boolean fifth;
}

The field declaration order and their order in memory layout are different:

OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     4       int FieldsArrangement.fourth                  N/A
     16     8    double FieldsArrangement.third                   N/A
     24     2      char FieldsArrangement.second                  N/A
     26     1   boolean FieldsArrangement.first                   N/A
     27     1   boolean FieldsArrangement.fifth                   N/A
     28     4           (loss due to the next object alignment)

The main motivation behind this is to minimize padding waste.

4.5. Locking

The JVM also maintains the lock information inside the mark word. Let's see this in action:

public class Lock {}

If we create an instance of this class, the memory layout for it would be:

Lock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 
      4     4        (object header)                           00 00 00 00
      8     4        (object header)                           85 23 02 f8
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes

However, if we synchronize on this instance:

synchronized (lock) {
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}

The memory layout changes to:

Lock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           f0 78 12 03
      4     4        (object header)                           00 70 00 00
      8     4        (object header)                           85 23 02 f8
     12     4        (loss due to the next object alignment)

As shown above, the bit-pattern for the mark word changes when we're holding the monitor lock.

4.6. Age and Tenuring

To promote an object to the old generation (in generational GCs, of course), the JVM needs to keep track of the number of survivals for each object. As mentioned earlier, the JVM also maintains this information inside the mark word.

To simulate minor GCs, we're going to create lots of garbage by assigning an object to a volatile variable. This way we can prevent possible dead code eliminations by the JIT compiler:

volatile Object consumer;
Object instance = new Object();
long lastAddr = VM.current().addressOf(instance);
ClassLayout layout = ClassLayout.parseInstance(instance);

for (int i = 0; i < 10_000; i++) {
    long currentAddr = VM.current().addressOf(instance);
    if (currentAddr != lastAddr) {
        System.out.println(layout.toPrintable());
    }

    for (int j = 0; j < 10_000; j++) {
        consumer = new Object();
    }

    lastAddr = currentAddr;
}

Every time a live object's address changes, that's probably because of minor GC and movement between survivor spaces. For each change, we also print the new object layout to see the aging object.

Here's how the first 4 bytes of the mark word changes over time:

09 00 00 00 (00001001 00000000 00000000 00000000)
              ^^^^
11 00 00 00 (00010001 00000000 00000000 00000000)
              ^^^^
19 00 00 00 (00011001 00000000 00000000 00000000)
              ^^^^
21 00 00 00 (00100001 00000000 00000000 00000000)
              ^^^^
29 00 00 00 (00101001 00000000 00000000 00000000)
              ^^^^
31 00 00 00 (00110001 00000000 00000000 00000000)
              ^^^^
31 00 00 00 (00110001 00000000 00000000 00000000)
              ^^^^

4.7. False Sharing and @Contended

The jdk.internal.vm.annotation.Contended annotation (or sun.misc.Contended on Java 8) is a hint for the JVM to isolate the annotated fields to avoid false sharing.

Put simply, the Contended annotation adds some paddings around each annotated field to isolate each field on its own cache line. Consequently, this will impact the memory layout.

To better understand this, let's consider an example:

public class Isolated {

    @Contended
    private int v1;

    @Contended
    private long v2;
}

If we inspect the memory layout of this class, we'll see something like:

Isolated object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12   128        (alignment/padding gap)                  
    140     4    int Isolated.i                                N/A
    144   128        (alignment/padding gap)                  
    272     8   long Isolated.l                                N/A
Instance size: 280 bytes
Space losses: 256 bytes internal + 0 bytes external = 256 bytes total

As shown above, the JVM adds 128 bytes of padding around each annotated field. Cache line size in most modern machines is around 64/128 bytes, hence the 128 bytes padding. Of course, we can control the Contended padding size with -XX:ContendedPaddingWidth tuning flag.

Please note that the Contended annotation is JDK internal, therefore we should avoid using it.

Also, we should run our code with the -XX:-RestrictContended tuning flag; otherwise, the annotation wouldn't take effect. Basically, by default, this annotation is meant for internal-only usage, and disabling the RestrictContended will unlock this feature for public APIs.

4.8. Arrays

As we mentioned before, the array length is also part of the array oop. For instance, for a boolean array containing 3 elements:

boolean[] booleans = new boolean[3];
System.out.println(ClassLayout.parseInstance(booleans).toPrintable());

The memory layout looks like:

[Z object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 # mark
      4     4           (object header)                           00 00 00 00 # mark
      8     4           (object header)                           05 00 00 f8 # klass
     12     4           (object header)                           03 00 00 00 # array length
     16     3   boolean [Z.<elements>                             N/A
     19     5           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 5 bytes external = 5 bytes total

Here, we have 16 bytes of object header containing 8 bytes of mark word, 4 bytes of klass word, and 4 bytes of length. Immediately after the object header, we have 3 bytes for a boolean array with 3 elements.

4.9. Compressed References

So far, our examples were executed in a 64-bit architecture with compressed references enabled.

With 8 bytes alignment, we can use up to 32 GB of heap with compressed references. If we go beyond this limitation or even disable the compressed references manually, then the klass word would consume 8 bytes instead of 4.

Let's see the memory layout for the same array example when the compressed oops are disabled with the -XX:-UseCompressedOops tuning flag:

[Z object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 # mark
      4     4           (object header)                           00 00 00 00 # mark
      8     4           (object header)                           28 60 d2 11 # klass
     12     4           (object header)                           01 00 00 00 # klass
     16     4           (object header)                           03 00 00 00 # length
     20     4           (alignment/padding gap)                  
     24     3   boolean [Z.<elements>                             N/A
     27     5           (loss due to the next object alignment)

As promised, now there are 4 more bytes for the klass word.

5. Conclusion

In this tutorial, we saw how the JVM lays out objects and arrays in the heap.

For a more detailed exploration, it's highly recommended to check out the oops section of the JVM source code. Also, Aleksey Shipilëv has a much more in-depth article in this area.

Moreover, more examples of JOL are available as part of the project source code.

As usual, all the examples are available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
guest
0 Comments
Inline Feedbacks
View all comments