← Back to list

Troubleshooting Java Memory Leakage

Memory leakage in Java occurs when a program unintentionally retains references to no longer needed objects, preventing the Java Garbage…

Dennis Lee in Towards Dev · 2024-08-30 04:47 · 87 claps · 4.4 min read
#java #memory-leak #visualvm #jcmd
Open on Medium ↗

Troubleshooting Java Memory Leakage

Memory leakage in Java occurs when a program unintentionally retains references to no longer needed objects, preventing the Java Garbage Collector (GC) from reclaiming memory. This can lead to the following symptoms:

  • Increased memory consumption.
  • Performance degradation.
  • Application to crash due to OutOfMemoryException.

This post will cover the following to improve our diagnosis and remediate the problem:

  1. Trivial application with an embedded memory leakage problem.
  2. Tools to help conduct memory leakage analysis.
  3. Remediate the problem.

Trivial Application with an Embedded Memory Leakage Problem

The application will use a HashSet to produce a memory leakage problem.

In Java, a HashSet is a part of the Java Collections Framework and implements the Set interface. It stores a collection of unique elements, not allowing duplicate values. HashSet uses the .equals() method to check for duplicates.

Armed with this information, class Foo is created without overriding .equals() . In theory, the HashSet is unable to distinguish Foo instances with the same value. Therefore adding this to the HashSet will result in the size growth thus producing a memory leakage.

import java.util.HashSet;
import java.util.Set;
import java.util.stream.IntStream;

public class MemLeakApplication {

 static class Foo {
  private String value;

  public Foo(String value) {
   this.value = value;
  }

  public String getValue() {
   return this.value;
  }
 }

 public static void main(String[] args) {
  Set<Foo> set = new HashSet<>();

  IntStream.range(0, Integer.MAX_VALUE)
    .forEach(it -> {
     Foo foo = new Foo("KEY");
     set.add(foo);

     System.out.println("Set size: " + set.size());
    });
 }
}

Tools to Help Conduct Memory Leakage Analysis

This post will cover several options to conduct the analysis:

  • jcmd
  • Visual VM

JCMD

jcmd is a command-line tool included in the Java Development Kit (JDK) for sending diagnostic commands to Java applications on the Java Virtual Machine (JVM). It helps troubleshoot performance issues, manages Java applications through various commands (like garbage collection and thread dumps), and collects performance metrics for monitoring and tuning.

# Identify the process ID (PID) of the Java Application i.e. MemLeakApplication.
> jcmd
1537 Eclipse
4679 MemLeakApplication

# Perform a heap dump
> jcmd 4679 GC.heapdump memleak.hprof

# Get a histogram of object types in the Java heap of a running Java application
> jcmd 4679 GC.class_histogram > memleak.histo

The histogram has identified a huge number of Foo instances, which is expected due to the missing .equals() .

> head -n 20  memleak.histo
4679:
 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:      38991377     1247724064  java.util.HashMap$Node (java.base@18.0.2.1)
   2:      38990411      623846576  MemLeakApplication$Foo
   3:           258      268459424  [Ljava.util.HashMap$Node; (java.base@18.0.2.1)
   4:          6897         323232  [B (java.base@18.0.2.1)
   5:          1429         176072  java.lang.Class (java.base@18.0.2.1)
   6:          6813         163512  java.lang.String (java.base@18.0.2.1)
   7:           913          89176  [Ljava.lang.Object; (java.base@18.0.2.1)
   8:             7          33032  [C (java.base@18.0.2.1)
   9:           480          31944  [I (java.base@18.0.2.1)
  10:           982          31424  java.util.concurrent.ConcurrentHashMap$Node (java.base@18.0.2.1)
  11:            22          23584  [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@18.0.2.1)
  12:           259          12432  java.util.HashMap (java.base@18.0.2.1)
  13:           341          10912  jdk.internal.math.FDBigInteger (java.base@18.0.2.1)
  14:           362           8688  java.lang.module.ModuleDescriptor$Exports (java.base@18.0.2.1)
  15:           256           6144  java.lang.Long (java.base@18.0.2.1)
  16:           249           5976  java.util.ImmutableCollections$Set12 (java.base@18.0.2.1)
  17:            62           4960  java.net.URI (java.base@18.0.2.1)

Unfortunately, I could not find a tool in JDK to analyze the heap dump hence resorting to Visual VM. Kindly leave a comment if there is such a tool. Meanwhile the following is a screen capture from Visual VM after loading the .hprof file.

The analysis is the same as the histogram hence using the JDK solution should suffice most cases.

Visual VM

VisualVM is a visual tool that provides detailed information about Java applications while they are running on the Java Virtual Machine (JVM). It is primarily used for monitoring, troubleshooting, and profiling Java applications.

Here are some screen captures showing the metrics across a short period.

In particular, the heap space is growing due to the growing number Foo instances, whereas the stack memory remains near constant.

Metaspace is an area of memory used by the Java Virtual Machine (JVM) to store class metadata. Understanding Metaspace is crucial for efficiently managing memory, especially when large numbers of classes or dynamic class loading occur.

The memory histogram aligns with jcmd , both indicating the large number of Foo instances.

Remediate the problem

To resolve the memory leakage, the .hashcode() and .equals() is overridden as follows:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.IntStream;

public class MemLeakApplication {

 static class Foo {
  private String value;

  public Foo(String value) {
   this.value = value;
  }

  public String getValue() {
   return this.value;
  }

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

  @Override
  public boolean equals(Object obj) {
   if (this == obj)
    return true;
   if (obj == null)
    return false;
   if (getClass() != obj.getClass())
    return false;
   Foo other = (Foo) obj;
   return Objects.equals(value, other.value);
  }
 }

 public static void main(String[] args) {
  Set<Foo> set = new HashSet<>();

  IntStream.range(0, Integer.MAX_VALUE)
    .forEach(it -> {
     Foo foo = new Foo("KEY");
     set.add(foo);

     System.out.println("Set size: " + set.size());
    });
 }
}

Running the application again and conducting the analysis using JCMD yields the following results:

The heap memory did not ramp up as before, which indicates the HashSet was able to identify duplicate Foo instance after overriding the .equals().

The histogram further proves the number of Foo instances has decreased and garbage collection is successful.

Summary

In this post, both JCMD and Visual VM were able to pinpoint where the memory leakage was. In most cases the JCMD is more convenient since it is rolled out with the JDK and therefore easily accessible. Visual VM has an intuitive interface and conveys the same data as JCMD but with time series metrics. Visual VM may be unavailable due to the working environment such as Internet proxies.


메타데이터
post_id
edccef06d41d
slug
troubleshooting-java-memory-leakage-edccef06d41d
url
https://towardsdev.com/troubleshooting-java-memory-leakage-edccef06d41d
canonical_url
https://towardsdev.com/troubleshooting-java-memory-leakage-edccef06d41d
author_url
https://medium.com/@dennisholee
status
ok
fetched_at
2026-06-17 15:37:45