Background
The Java Virtual Machine(JVM) provides a runtime environment for our Java code. The JVM has two primary functions:
- It allows Java programs to run on any device in a platform independent manner. It does this by converting the compiled Java bytecodes into native machine code.
- It manages and optimizes program memory.
The JVM memory manager creates different memory pools at runtime. The most important pools are the stack and the heap.
- The stack is used for temporary storage of local variables and parameters. These are small data types such as primitives and object reference data types.
- The heap is used to store actual object instances, which can be of any size.
Memory Leaks
In every programming language, memory is a vital and scarce resource. So we, as programmers, must manage memory with care. We need to be thoughtful about the allocation and deallocation of memory. If we don’t manage memory properly, it can result in memory leaks.
As a Java application runs, it creates objects on the heap. When the objects are no longer in use, the garbage collector removes them from the heap. This then frees memory preventing leaks.
The garbage collector cannot remove objects if other objects still reference them.
A memory leak occurs when a program unintentionally refers to object that it no longer needs. This is usually due to logical coding errors. These object references stop the garbage collector from disposing of the objects.
As a result, the application consumes more and more resources as it runs. This eventually leads to a fatal OutOfMemoryError
.
To refresh your memory about garbage collection, see the blog post on garbage collection.
Symptoms of Memory Leaks
Memory leaks are bad! They can block file resources, reduce memory availability and degrade system performance.
How do we know if our application has any memory leaks? At first, our application will run as fast as expected, but its performance will start to slow down over time. It should work well on small data sets. As the size of the data sets increase, we will experience performance issues.
If we watch the JVM memory usage, we’ll see an ever-increasing usage of old/perm-generation memory in the JVM. After that, we’ll get unexpected crashes with fatal OutOfMemoryError
heap errors.
Causes of Memory Leaks
Many different coding issues can cause memory leaks. Let’s look at three:
- The use of large static fields.
- Incorrect or missing
equals()
andhashCode()
implementations. - Unclosed file resources.
The lifetime of static variables is usually the same as the lifetime of the running application. They live as long as the program runs unless we explicitly set them to null
. Forgetting to do this can cause memory leaks.
Incorrect equals()
and hashCode()
implementations can cause memory leaks. A common problem is if we put objects that are missing their hashCode()
or equals()
methods into a hash table. Memory leaks can occur when we put duplicate objects into the hash table. The table will keep on growing as we add duplicate objects. With correct hashCode()
and equals()
methods, the table will reject these duplicates.
If we forget to close file resources after we use them, the application will run out of open file handles. It will then start consuming more memory. We should use the try-with-resources statement when working with files. This will ensure that any opened resources are automatically closed.
Other common causes for memory leaks are:
- The creation of very large objects.
- The use of anonymous classes.
- The use of finalizers.
- Lots of large session objects in a web application.
- Insertion without deletion into
Collection
objects. This happens when we forget to remove objects from the collection even though we don’t need them any more. The garbage collector can’t remove these objects, because they’re still being referenced. - Unbounded caches.
- Listener methods that aren’t invoked.
Detecting and Fixing Memory Leaks
Detecting memory leaks can be difficult. During development we can use static code analysers. These tools can check for potential leaks. They can’t catch all the issues because we only get memory leaks while the application is running.
To find memory leaks, we can use a variety of runtime tools to help identify the source of the leak. Once we’ve found the code causing the leak, only then we can fix it. Three common detection techniques are:
- Using profilers.
- Enabling verbose JVM garbage collection messages.
- Analysing heap dumps.
Profilers watch Java bytecode constructs and operations at the JVM level. This includes object creation, iterative executions, method executions, thread executions, and garbage collections.
Java profilers give us a comprehensive set of data we can use to trace our coding mistakes. Profilers help us find memory leaks, thread problems and performance slowdowns. Profiling tools give us a fine-grained analysis of every problem, and an idea of how to solve them. Running a profiler and acting on the results can lead to a more stable and scalable application.
Conclusion
In the next few posts, we’ll look at some of the causes of memory leaks and their solutions in more detail. We’ll look at some of the tools we can use to identify leaks.
Please share your thoughts, questions and comments.
Stay safe and keep learning!