Memory Leaks in Java – Part 4

Memory leaks in Java - stylized image of a man at a huge dripping tap with a bucket to one side

This is part 4 of a multi-part series on memory leaks in Java. If you missed any of the last posts, you can find them here:

Last post we looked at how string interning can cause memory leaks. We also looked at the effect of string concatenation on performance. In this post, we’ll look at how incorrect or missing equals() and hashCode() methods can cause memory leaks.

Revision Of equals() And hashCode() Methods

The Java documentation says that the equals() method “implements an equivalence relation on non-null object references”. In other words, we use it to test whether two objects are “equal to” each other.

What “equal to” actually means is determined by the business rules of our application. These rules are generally different for each class in every application. We generally only override the equals() method for model/entity classes. The equals() method is used by collections that cannot contain duplicate elements. These include HashSet, HashMap, TreeSet and TreeMap.

The hashCode() method is another canonical method that we usually only override for model/entity classes. It should return an int value that uniquely identifies an object. The general contract for hashCode() says that equal objects must have equal hash codes. This implies that the same object fields used in the equals() method must be used in the hashCode() method. We must override the hashCode() method if we want to use hash tables in our application. Hash table implementations in Java include HashMap and HashSet.

We don’t need to return different hash code values if two objects are not equal according to the equals() method. However, if we do return different values, the hash table performance will improve.

If we override the equals() method in a class, we must also override the hashCode() method. If we don’t, our class will violate the general contract for hashCode(). If we then add objects of this class to a hash table, it won’t function properly.

Default Object Implementations

The Object implementations of these two methods are not suitable for business applications. The hashCode() method typically returns the physical address of the object in memory. The equals() method checks if two object references refer to the same object, i.e. it also checks the addresses. These are good default implementations, but not particularly useful in everyday applications.

Let’s say that we correctly override the equals() method in our class. This will mean that two different objects with the same contents would be logically equal to each other according to the equals() method.

Now let’s say that we didn’t override the hashCode() method, but used the inherited hashCode() method. The previous two equal objects would just be two different objects at different addresses. From the perspective of the hashCode() method, they would have nothing in common. Calling the hashCode() method would then return two apparently random values. Two equal values should be returned according to the contract.

Code Example Without equals() And hashCode() Methods

Let’s create a very simple Key class to use as a key in a Map. We will not override the equals() and hashCode() methods:


/** A very simple class to be used as a key when inserting values into
  * a HashMap. This class has no equals() or hashCode() methods. This 
  * will easily trigger a memory leak when inserting values into a 
  * HashMap, because whenever a new Key object is created, the HashMap
  * sees the object as a unique object, even if the string used in the
  * constructor is the same.
  */

import java.util.Objects;

public class Key {

    private final String value;

    public Key(String value) {
        // simple null check
        this.value = Objects.requireNonNull(value);
    }

    // no equals() and no hashCode() methods

} // end of class

Now let’s use this Key class as a key when putting values into a HashMap. A HashMap can’t contain duplicate keys. Because we haven’t implemented the equals() method, the HashMap has no way of knowing if we create duplicate Key objects:

import java.util.Map;
import java.util.HashMap;

public class MemoryLeakTest {

    public static void main(String args[]) {

        final int NUMBER = 100;

        Map<Object, String> map = new HashMap<>();
        for(int i=0; i<NUMBER; i++) {
            map.put(new Key("key"), "some value");
        }

        // Checking how many objects were inserted. 
        // Without the equals() and hashCode(), NUMBER are inserted.
        // With the equals() and hashCode(), only 1 object is inserted.
        System.out.println("map.size() = " + map.size());

        // Searching for the object with get()
        String value = map.get(new Key("key"));
        if (value == null)
            System.out.println("No objects found with this key");
        else
            System.out.println("Object found, value = |" + value +"|");

        // Searching for the object with containsKey()
        boolean isFound = map.containsKey(new Key(null));
        if (isFound)
            System.out.println("Object found!");
        else
            System.out.println("No objects found with this key");
    }
} // end of class

The program prints out the following:

map.size() = 100
No objects found with this key
No objects found with this key

Since the HashMap doesn’t allow duplicate keys, the many duplicate Key objects that we used as keys shouldn’t increase the memory. But because we didn’t define the equals() and hashCode() methods, these duplicate objects are inserted. This is why we see more than one object in the HashMap. This will increase the memory usage, and could cause a memory leak.

The HashMap::put() method calls both the equals() and the hashCode() methods of the Key class when inserting objects. If either or both methods are missing, every Key object will be inserted in the loop. This will result in a high probability of memory leaks.

The HashMap::get() method also uses the equals() and hashCode() methods of the Key class. If either or both methods are missing, then objects can’t be found. This is because the Key object used to search for an entry is a unique object, and not one contained in the map.

Implementing equals() And hashCode() Methods

Now let’s implement the equals() and hashCode() methods in the Key class:

public class Key {

    private final String value;

    public Key(String value) {
        // simple null check
        this.value = Objects.requireNonNull(value);
    }

    @Override 
    public int hashCode() { 
        return value.hashCode();
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Key)) {
            return false;
        }
        Key tmp = (Key) o;
        return tmp.value.equals(value);
    }
} // end of class

Re-running the test program will print the following:

map.size() = 1
Object found, value = |some value|
Object found!

Now the HashMap works properly, and rejects the duplicate keys. This shows the importance of correctly implementing the canonical equals() and hashCode() methods.

Bonus Code Example

To see what can happen with uncontrolled growth of a Map, comment out the equals() and hashCode() methods in the Key class, and change the main() method to:

public static void main(String args[]) {
    int counter = 0;
    try {
        Map<Object, Object> map = System.getProperties();
        while (true) { 
            ++counter;
            map.put(new Key("key"), "some value");
        }
    }
    catch (Throwable e) {
        System.out.println("Counter = " + counter);
        e.printStackTrace();
    }
}

Start up the JVM with a very small heap, else you’ll wait forever as it fills up memory.

Running with Java 8:

c:> java -Xmx2m -Xms2m MemoryLeakTest

Counter = 18376
java.lang.OutOfMemoryError: Java heap space
        at java.util.Hashtable.rehash(Hashtable.java:401)
        at java.util.Hashtable.addEntry(Hashtable.java:425)
        at java.util.Hashtable.put(Hashtable.java:476)
        at MemoryLeakTest.main(MemoryLeakTest.java:103)

Running with Java 17:

C:> java -Xmx2m -Xms2m MemoryLeakTest

Exception: java.lang.OutOfMemoryError thrown from 
the UncaughtExceptionHandler in thread "main"

Conclusion

For revision on canonical classes, see the blog post.

In the next post, we’ll look at some of the tools we can use to identify leaks. This includes profilers and verbose garbage collection messages.

Please share your comments.

Stay safe and keep learning!

Leave a Comment

Your email address will not be published. Required fields are marked *

Code like a Java Guru!

Thank You

We're Excited!

Thank you for completing the form. We're excited that you have chosen to contact us about training. We will process the information as soon as we can, and we will do our best to contact you within 1 working day. (Please note that our offices are closed over weekends and public holidays.)

Don't Worry

Our privacy policy ensures your data is safe: Incus Data does not sell or otherwise distribute email addresses. We will not divulge your personal information to anyone unless specifically authorised by you.

If you need any further information, please contact us on tel: (27) 12-666-2020 or email info@incusdata.com

How can we help you?

Let us contact you about your training requirements. Just fill in a few details, and we’ll get right back to you.

Your Java tip is on its way!

Check that incusdata.com is an approved sender, so that your Java tips don’t land up in the spam folder.

Our privacy policy means your data is safe. You can unsubscribe from these tips at any time.