Skip to main content

Command Palette

Search for a command to run...

Is Your Java Application Leaking or Just Bloated? The 5 Common Memory Hogs You're Missing

Find out how a few simple tweaks can result in huge savings

Published
7 min read
Is Your Java Application Leaking or Just Bloated? The 5 Common Memory Hogs You're Missing

Does your application throw OutfMemoryErrors? Does it experience slow response times due to excess garbage collection? Are your cloud costs too high because you’re using lots of memory? Or does the program need to run on small devices, and the available memory is just not enough?

Perhaps you’ve searched exhaustively for memory leaks, and you can’t find any. If your program is using too much Java heap space, it may simply be that you’re wasting memory without knowing it.

In this article, we’ll look at some of the common causes of wasted memory, how to find where your application is wasting space, and include some hints for fixing the problem.

Common Causes of Wasted Java Heap Space

If you’re experiencing OutOfMemoryErrors related to heap space, you may like to read this article: Java OutOfMemoryError: Heap Space. It includes common causes, debugging strategies and suggested fixes.

If the problem isn’t a memory leak, you’re probably wasting space. Here are five common issues that can cause this problem.

1. The Heap is Incorrectly Sized

We can specify the size of the Java heap space by using command line arguments when we invoke the JVM.

It’s important to size it correctly. If we don’t allocate enough space, the application is likely to throw OutOfMemoryErrors, or experience poor performance because the garbage collector is overworking.

On the other hand, if too much heap space is allocated, unused space is simply wasted. Other applications can’t access that memory, and, if we’re running in the cloud, we will have to pay for it even though we don’t need it.

To learn how to accurately determine your application’s memory requirements, and adjust the size of the Java heap space, you may like to read this article: Sizing Your Heap Correctly.

2. Memory is Not Being Released

The garbage collector works constantly in the background, removing any items that are no longer in use to free space for new objects. To do this, it checks which objects still have valid references pointing to them. References become invalid when:

  • They go out of scope. This happens when the block of code holding the reference completes.

  • They are explicitly set to null.

To make sure objects can be garbage collected, we need to make sure that objects are defined within the correct scope. If something is defined as a class variable, but it’s only used in a single method, it will be retained for the duration of the program regardless.

If we’ve finished with an object before the block of code completes, we can set it to null to ensure it’s garbage collected quickly.

It’s also important to check whether a class has a close method, and call it as needed. The close method generally releases memory required by the class. This is especially true of streams, which may retain a large amount of memory if not closed.

3. Duplicates

Far too many programs hold the exact same information twice – or even many times. This can add up to huge memory wastage. The duplicates may be strings, objects or arrays. Strings are a common culprit. Later in the article, we’ll look at how to identify these duplicates.

Here are some hints to help you eliminate duplicate data from your application.

  • For duplicate strings, Java’s facility for string deduplication can save a lot of space. To learn about this feature, see this article: String Deduplication in Java.

  • If a large string, array or object is used in several places, and its value doesn’t change, we can consider storing it as a constant.

  • Storing the object in a central cache can often be a good tactic.

  • Pooling can be helpful, for example, database connection pools.

4. Inefficient Collections

Using Java collections can save a lot of coding and development time. However, they do come with an overhead. HashMaps, for example, maintain a complex system of buckets to ensure data can be retrieved quickly. If we know exactly how many items we’ll be storing, we can often save a lot of memory by simply using an array.

If we decide a collection is the right solution for an application, we should take time to understand the structure and growth strategy of the collection type to make sure we don’t waste memory.

For example, a HashMap is automatically created with an initial size of 16 if we don’t specify a size. If we only need three items in the map, 13 slots are wasted. Furthermore, we may create a HashMap and never add any items to it at all, if that functionality is not needed in a given scenario.

Every time the HashMap is full, it expands itself by doubling its size. This can result in a lot of wasted space. If we currently have 4096 items in the map, and we add one more, it will then have a size of 8192. If we end up with 5000 items in the map, 3192 slots will be wasted.

ArrayLists are created with a default of 10 items, and also double in size whenever they need to expand.

There are a couple of things we can do to minimize this problem.

4.1 Supply an Initial Size to the Constructor

Most collections have a constructor form that allows us to specify an initial size. To create an ArrayList that has an initial size of 6, the constructor would be coded as new ArrayList<>(6).

It’s worth taking the time to work out a sensible initial size for collections. If necessary, we could use a configuration parameter to determine the initial size. This would eliminate the need for changing the code if, at a later time, the application needs a larger collection.

4.2 Use Lazy Instantiation

To prevent the issue of empty collections, we can code so that the collection is only instantiated at the time the first item is added to it. The coding could look like this:

private Map thisMap;        
public void addData(String key, String value) {        
    if (thisMap == null) {    
        thisMap = new HashMap<>();
    }    
    thisMap.put(key, value);    
}

5. Object Headers

Every object in Java has an object header. The size depends on the type and version of the JVM, but it’s usually 12 bytes. These headers can add up to a lot of wasted space.

A number defined as the Java primitive byte occupies one byte. However, a number defined as the wrapper class Byte occupies 13 bytes: one for the data, and 12 for the object header. Take a look at the following code.

Byte[] myArray = new Byte[100000];

This uses 1,200,000 more bytes than an array of the Java primitive byte.

This practice is known as boxing numbers, and we should avoid it wherever possible.

It’s also worth considering amalgamating small objects that hold only one or two variables into larger objects. This reduces the number of object headers.

How to Check What’s Wasting Java Heap Space in our Applications

The first step is to take a heap dump. The following articles explain how to do this:

Next, we need a heap dump analyzer tool such as HeapHero or Eclipse MAT. These produce reports that allow us to easily see what’s happening in the Java heap. Look at the largest objects, and see whether they really need to be that big.

HeapHero goes one step further, and detects wasted space.

It first shows an overview of how much space is wasted by cause. Here’s a sample:

Fig: Overview of Wasted Memory Produced by HeapHero

It then shows fine details of all instances of wasted space in each category. The example below shows the details of inefficient collections and who is holding them, with a link to hints on how to fix inefficient collections.

Fig: Details of Inefficient Collections Shown by HeapHero

Conclusion

Saving memory really does matter in today’s computing environment. To keep applications efficient and cost-effective, we need to make sure we’re not wasting Java heap space.

HeapHero provides a quick way of identifying wasted space in our programs, as well as giving recommendations on how to solve each problem.