Fully featured low overhead profiler for Java EE and Java SE platforms.
Easy-to-use performance and memory .NET profiler for Windows, Linux and macOS.
Secure and easy profiling in cloud, containers and clustered environments.
Performance monitoring and profiling of Jenkins, Bamboo, TeamCity, Gradle, Maven, Ant and JUnit.

Shallow and retained sizes : Understanding memory usage in Java applications

One of the crucial aspects of developing efficient and high-performing Java applications is keeping a close eye on memory usage. In Java, there are different ways to measure the memory footprint of an object, and one of the most important measures is the retained object size. In this article, we will explore the concept of retained object size, how it differs from the shallow size of an object, and why it is crucial in memory management and optimization.

What is retained object size?

Retained object size refers to the total memory that is held up by an object, including all the objects that would be rendered unreachable if the primary object were to be removed from memory. In other words, the retained size of an object is the memory space occupied by that object and all other objects that are exclusively reachable from it.

To provide a clearer understanding, let's consider an analogy. Imagine a tree with branches, leaves, and roots. In this analogy, the tree trunk represents our primary object, the branches and leaves represent the objects directly or indirectly referenced by the primary object, and the roots represent the objects that keep the primary object reachable. If we were to remove the tree trunk (our primary object), the branches and leaves (objects referenced by the primary object) would also fall, as they are exclusively supported by the trunk. The retained size of the tree trunk would include the space occupied by the branches and leaves as well, as they are exclusively reachable from the trunk.

How does retained size differ from shallow size?

Shallow size, in contrast to retained size, only measures the memory occupied by the object itself, excluding the memory taken up by objects it references. It considers only the immediate memory consumption of an object without accounting for the memory used by objects it indirectly holds.

Let's go back to our tree analogy. The shallow size of the tree trunk would only consider the space occupied by the trunk itself, not including the branches and leaves. In contrast, the retained size of the tree trunk would encompass the trunk, branches, and leaves altogether, as they all rely on the trunk.

In terms of a Java object, the shallow size includes only the memory used by the object's fields, plus some overhead for the object header. If the object has reference fields pointing to other objects, the memory used by those referenced objects is not included in the shallow size. In contrast, the retained size of an object includes the memory used by all objects that would become unreachable if the object were removed from memory.

In order to measure the retained sizes, all objects in memory are treated as nodes of a graph where its edges represent references from objects to objects. There are also special nodes - GC roots, which will not be collected by Java garbage collector. GC roots are objects that are considered the starting points for the garbage collector to determine if an object is reachable or not. Examples of GC roots include active threads, static variables, local variable on stack, and JNI references.

The pictures below show the same set of objects, but with varying internal references:

Figure 1:
Figure 2:
Retained objects
Retained objects

As you can see, in both pictures we have highlighted all the objects that are directly or indirectly accessed only by obj1. If you look at Figure 1, you will see that obj3 is not highlighted, because it is also referenced by a GC root object. On Figure 2, however, it is already included into the retained set, unlike obj5, which is still referenced by GC root.

Thus, the retained size of obj1 will represent the following respective values:

  • For Figure 1: the sum of shallow sizes of obj1, obj2 and obj4.
  • For Figure 2: the sum of shallow sizes of obj1, obj2, obj3 and obj4.

Looking at obj2, however, we see that its retained size in the above cases will be:

  • For Figure 1: the sum of shallow sizes of obj2 and obj4
  • For Figure 2: the sum of shallow sizes of obj2, obj3 and obj4

Why is retained size important?

Understanding the retained size of an object is critical for several reasons:

1. Memory leak detection: By analyzing the retained size of an object, you can identify potential memory leaks. Objects with unexpectedly large retained sizes might indicate a memory leak, as they hold on to other objects that should have been released.

2. Memory usage optimization: Knowing the retained size of objects allows to identify potential optimization opportunities. By analyzing the objects held by a particular object, you can optimize the data structures and improve the overall memory usage of your Java applications.

3. Garbage collection efficiency: The retained size of an object can impact the efficiency of Java's garbage collector. Objects with large retained sizes can cause the garbage collector to work harder, impacting the application's performance. By optimizing the retained size of objects, you can improve the efficiency of garbage collection and enhance the performance of your applications.

YourKit uses cookies and other tracking technologies to improve your browsing experience on our website, to show you personalized content, to analyze our website traffic, and to understand where our visitors are coming from.

By browsing our website, you consent to our use of cookies and other tracking technologies in accordance with the Privacy Policy.