How Much Memory Does Your Java Object Occupy?

In this article, we’ll talk about how JVM stores the objects inside memory: their alignment. Object representation is an essential topic for understanding the underlying mechanisms of JVM, which provides insights to help with application tuning.

Here, we’ll concentrate mainly on the padding and alignment, not how JVM represents objects in memory. To get more information about memory layout, please check this other article.

Object Alignment

Operational systems use alignment for caching, performance reasons, and hardware efficiency. Although JVM is an abstraction over the underlying operational system, internally, it should handle the alignment similarly for the same reasons. 

The object alignment is configurable on JVMs. We can use -XX:ObjectAlignmentInBytes to provide a custom value. The alignment should be a power of two and cannot exceed four bytes on 64-bit architectures. 

Memory Consumption

At the same time, padding is the memory that doesn’t contain any useful information, and based on the alignment size, the padding size might vary. Let’s say we have the following object:

public class SimpleObject {
public int i;
}

Let’s make the calculations based on the components of 32-bit systems with 4-byte alignment:

Empty Object’s Size = Mark Word + Klass Pointer + Padding + Instance fields + Padding

Simple int holder on a 32-bit architecture with 4-byte alignment
Fig 1: Simple int holder on a 32-bit architecture with 4-byte alignment

JVM ignores padding when objects naturally align to 4 bytes. 

Let’s make similar calculations for 64-bit architectures with 8-byte alignment and 4-byte (compressed) pointers:

Simple int holder on a 64-bit architecture with 8-byte alignment
Fig 2: Simple int holder on a 64-bit architecture with 8-byte alignment

In this case, the object is misaligned and requires an additional eight padding bytes. Thus, the padding will take a third of the object size. Although it sounds like a huge waste, it’s significant only for small objects, and the padding size cannot be greater than the alignment.

At the same time, it means that the following class on 64-bit architectures won’t take any extra space:

public class SimpleObject {
public int i;
public bool b;
}

This class will have the following layout. In most cases, a boolean takes up a byte for performance reasons:

Simple int and bool holder on a 64-bit architecture with 8-byte alignment
Fig 3: Simple int and bool holder on a 64-bit architecture with 8-byte alignment

The object’s size didn’t change as we used the area previously allocated for padding. At the same time, on 32-bit systems, we’ll see another picture:

Simple int and bool holder on a 32-bit architecture with 8-byte alignment
Fig 4: Simple int and bool holder on a 32-bit architecture with 8-byte alignment

Adding another bool field results in the four-byte increase in the object size: one byte for the boolean itself and three bytes for padding.

On the 64-bit systems, the mark word will take up eight bytes. The size of the class pointer may differ based on the alignment configuration, heap size, and the use of compressed pointers. If we turn the compressed pointers off, we will get the following layout:

Simple int and bool holder on a 64-bit architecture with 8-byte alignment
with uncompressed (8-byte) pointers

Fig 5: Simple int and bool holder on a 64-bit architecture with 8-byte alignment with uncompressed (8-byte) pointers

Because of the padding we used previously, the object’s size didn’t grow. We can imagine how the compression can affect a large array of reference types, for example, Strings:

public class Person {
private String firstName;
private String lastName;
private Address address;
// constructors, getters, setters, etc.
}

With compressed pointers on 64-bit systems, the object layout would look like this:

Person object on 64-bit architecture 
with 8-byte alignment and compressed pointers

Fig. 6: Person object on 64-bit architecture with 8-byte alignment and compressed pointers

However, if we turn them off, the size of the object would increase:

Person object on 64-bit architecture 
with 8-byte alignment and uncompressed pointers

Fig. 7: Person object on 64-bit architecture with 8-byte alignment and uncompressed pointers

Uncompressed references would add eight additional bytes in this case. However, if we imagine a List or array of reference types, we can see how it might impact our application.

Reference Size and Performance

It doesn’t mean that 64-bit systems are slower because they consume more memory. The mark word can contain more information about the object, like hash codes and data connected to the garbage collection, preventing round trips or additional lookups. 

1. Low Creation Rate

Let’s first review a benchmark that doesn’t produce much garbage:

@Benchmark
public void filteringList(NumberFilteringState state, Blackhole blackhole){
filterList(state.integers, blackhole);
}

The filterList method separates even and odd numbers of a large list:

private static void filterList(List<Integer> integers, Blackhole blackhole)
{
int even = 0;
int odd = 0;
for (final Integer integer : integers) {
if (integer % 2 == 0) {
even++;
} else {
odd++;
}
}
blackhole.consume(even);
blackhole.consume(odd);
}

We can compare its performance using different alignment sizes and compressed and non-compressed pointers. However, the fact that we’re using compression causes the most differences:

Object Alignment Performance (8 GB heap)

Object AlignmentCompressed pointers (ops/s)Uncompressed pointers (ops/s)
8 bytes193.393242.080
16 bytes194.309243.021
32 bytes194.631242.330

2. High Creation Rate

Now, let’s review a similar setup using another benchmark. This benchmark would have a high creation rate:

@Benchmark
public void creatingList(NumberFilteringState state, Blackhole blackhole) {
List<Integer> linkedList = createLinkedList();
blackhole.consume(linkedList);
}

This benchmark uses the following method to create a LinkedList with one million random Integers:

@NotNull
private static LinkedList<Integer> createLinkedList() {
return new LinkedList<>(ThreadLocalRandom.current()
.ints(ONE_MILLION)
.boxed()
.collect(Collectors.toList()));
}

If we analyze the performance, we’ll get a different picture:

Object Alignment Performance (8 GB heap)

Object AlignmentCompressed pointers (ops/s)Uncompressed pointers (ops/s)
8 bytes30.35229.026
16 bytes30.74728.922
32 bytes30.74628.772

Interaction With Compressed Pointers

Interestingly, by increasing the alignment size, we can reduce memory consumption. It happens because we reduce the number of places to put an object. Which at the same time leads to the ability to use compressed pointers. 

For example, for a 64 GB heap with 16-byte alignment, we can use a 32-bit pointer with compression. We don’t need to store the last four bits of the address. It’s similar to the usually compressed pointers JVM uses by default, but we can compress them even more since we changed the alignment. 

For instance, we can reference an object with a 48-byte offset with a 28-bit pointer:

4-bit compression
Fig. 8: 4-bit compression

When using 16-byte alignment, the four least significant bits will always be zero, so we can ignore them and store only 28 bits. To get the original reference back, we should just make a bit-shift to restore it.

Conclusion

Understanding objects’ alignment provides us with insights into the inner workings of JVM. This knowledge might benefit general understanding and give us more context of tuning and different VM arguments often used for performance configuration.

However, we must test all the ideas for improving a specific application’s performance with dedicated tools. The yCrash platform provides a comprehensive analysis of an application, which we can use to test hypotheses before committing to it.

Share your Thoughts!

Up ↑

Index

Discover more from yCrash

Subscribe now to keep reading and get access to the full archive.

Continue reading