Here are the principles of detecting memory leak issues with Profiler. Detecting a memory leak is done by the following steps.
- Monitor JVM heap size and its trend.
- If the used heap size continues to increase, check which object occupies most of JVM heap.
- Look at how/where the object is allocated and released.
- Check if the object is allocated and released appropriately in the inline code.
How do I complete the above steps with Profiler? We can do by looking at the several graphs of Profiler.
Heap graph in 'Profiler' window
First, let's look at the 'Profiler' window that is located at the
bottom of Netbeans windows. The left figure is the graph of Heap size
and used heap. Then you can see how heap size and used heap have been
changed through your application execution. If used heap is
increasing over time, it has possibly a memory leak problem.
The figure above briefly explains how JVM uses memory. In this case,
used heap in the left graph is relatively stable. Because it does not
keep growing over time.
Live Objects Memory graph
Next, let's look at the memory graph to see which object is occupying
the most of memory. In 'Live Bytes' field, the graph shows the memory
size used by each objects. 'Live Objects' field represents the number
of objects which are in JVM heap. According to them, We can see which
object occupies memory. Please also look at the 'Generations'
field. 'Generations' represents number of survived generations. If
objects of a class continue to be allocated over time and are not
released, the number of 'Generations' will keep growing. This is a
typical memory leak situation, and the field will tell you that.
In above 'Live Objects Memory graph' example, HashMap$Entry occupies more area of memory. However, the number in 'Generations' field is low. It's hard to conclude if there is a memory leak. We need to run the application longer time and monitor the memory usage.
Reverse call graph
If there is any suspicious object, then, let's look at the reverse
call graph. It helps us to see where the objects that are occupying
memory are allocated. It will give us ideas what we should look at in
our java code.
The above figure shows a reverse call graph of 'char' object. The
graph indicates that 'char' is allocated in
StringBuffer.expandCapacity(int) method. By reverse call graph, we can
specify where the object in question is allocated. Then we look at the
inline code at the place.
Next, let me introduce a case study of detecting a memory leak problem
with Profiler. NetBeans Profiler has the functionality of object
liveness profiling (Record both object creation and garbage
collection). It will show you the statistics of allocating and
deallocating objects to help you detect any memory issue.
Sample code
To demonstrate detecting a memory leak with Profiler, I use the
following sample code.
[TestHash Sample Code]
/*
* Main.java
*
* Created on 2005/01/06, 11:00
*/
package memoryleak;
import java.util.*;
import java.io.*;
/**
*
* @author root
*/
public class Main {
Hashtable hashtable = new Hashtable();
Square sq[] = new Square[10];
/** Creates a new instance of Main */
public Main() {
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
// TODO code application logic here
Main testhash = new Main();
for(int i=0; i<100000; i++) {
testhash.create(i);
testhash.use(i);
testhash.release();
}
}
// Create Square objects.
void create(int i) {
for(int j=0; j<10; j++) {
long index = j+i*10;
sq[j] = new Square(index);
hashtable.put(sq[j].num, sq[j]);
}
}
// Use Square objects.
void use(int i) {
for(int j=0; j<10; j++) {
System.out.print(((Square)(hashtable.get(sq[j].num))).square + " ");
}
System.out.println();
}
void release() {
for(int j=0; j<10; j++) {
sq[j] = null;
}
}
}
class Square {
String num;
String square;
public Square(long num) {
this.num = new Long(num).toString();
this.square = new Long(num*num).toString();
}
}
Assume the code works in the following way, although you may already notice the problem of this sample code. :)
- Create Square objects.
- Store created Square objects in Hashtable.
- Release Square objects after using them.
Finally, it releases Square objects, so it seems to have no memory leak issue.
Problem: OutOfMemoryError
Now, let's run the sample code. If there is no problem, it should display the square of numbers through 1 to 999999. Here is the execution result.
[Execution Result]
0 1 4 9 16 25 36 49 64 81
:
snip
:
179411544900 179412392041 179413239184 179414086329 179414933476
179415780625 179416627776 179417474929 179418322084 179419169241
Exception in thread "main" java.lang.OutOfMemoryError
Unfortunately, the application is terminated due to OutOfMemoryError Exception.
Use Profiler
Why the sample application was terminated due to OutOfMemoryError?
We can guess the application exhausted the jvm heap, because it is
OutOfMemoryError. However, how and which object causes this error?
This is the time to use NetBeans Profiler. It profiles allocating and
deallocating new objects. To run the profiler, select the
application's main class in the Explorer window and choose Profile ->
Profile File...
Then, choose Analyze Memory Usage. Choose Record both objects and garbage collection.
Heap graph in 'Profiler' window
The application is running with the Profiler. To see the profiler statistics, choose Profile -> Get Current Results. As the first step, look at 'Profiler' window at the bottom of whole window. In Heap graph in 'Profiler' window, we can see the size of used heap (purple color) continue to grow. So, it has possibly a memory leak problem.
[Current Results image]
Live Objects Memory graph
Next, look at the Live Objects Memory graph in the above figure. There
are four objects that occupy the most of memory. They are char[],
String, Hashtable$Entry and Square. Also look at the 'Generations'
field in the above figure. This field essentially represents the sign
of a memory leak. If this number is increasing steadily, it likely
means the existence of a memory leak. The four objects have a higher
number in the Generations field. They are 51. By contrast, others are
1. Due to the higher number, I suspects they have not been released
correctly and caused a memory leak.
Reverse call graph
Now, let's look at the reverse call graph of the 'char' object which
is at the top of the list. You can do it by double-clicking the 'char'
object item.
[Reverse call graph of char]
According to the reverse call graph, it seems that some of 'char'
objects were created in String.<init>, but have never been
released. Because 'char' created in Square.<init> has the same number
in both 'Live objects' and 'Allocated objects' fields. Now, let's look at the reverse
call graph of 'String' object.
[ Reverse call graph of String]
There is a similar situation to 'char'. Some of 'String' objects were
created in Long.toString(), but have not been released. 'String'
created in Long.toString() has the same number of Live and Allocated
objects. Why these objects have never been released? The clue is in
the list of live objects. In the list, 'Hashtable.$Entry' object also
has higher Generations field. 'Hashtable.$Entry' is alive till
Hashtable.remove() is called. And 'Hashtable.$Entry' has objects
which are stored in Hashtable. Therefore, we can guess that objects
which are stored in Hashtable have not been released appropriately.
Look at the inline code.
Then, let's go back to the source code of the sample program.
[Excerpt of source]
void release() {
for(int j=0; j<10; j++) {
sq[j] = null;
}
}
Good! Now, we are very close to the root cause. 'Square' objects are
correctly set to 'null'. However, we seemed to forget to release these
'Square' objects in Hashtable. The Hashtable has you. We can see a
'Square' object has a 'String' object. 'String' objects in 'Square'
objects seem to remain in heap. To resolve this issue, I modify the
'release' method in the source code in the following way.
[Modified source]
void release() {
for(int j=0; j<10; j++) {
hashtable.remove(sq[j].num); /* added */
sq[j] = null;
}
}
Let's run the corrected sample program.
[Result]
0 1 4 9 16 25 36 49 64 81
:
snip
:
999980000100 999982000081 999984000064 999986000049 999988000036
999990000025 999992000016 999994000009 999996000004 999998000001
Wow, We see the sample program finished without any error. With the functionality of monitoring memory usage, NetBeans Profiler allows us to dig into the cause of memory leak problem.