I've spent the past 2 days trying to debug a memory leak in my application and have now narrowed it down to one JFrame that is not being properly disposed.
It is started from a Control Frame via the following snippet:
startButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
new LeakingInterface();
}
});
The Frame itself has setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
set.
It contains a Button that saves the changes made within the Frame and then disposes as well as the typical close button. The "save changes"-button is worded as follows:
saveChanges.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
saveChanges();
dispose();
}
});
Now to my problem: The Frame is not properly unloaded from memory. I debugged this with the Eclipse Memory Analyzer. When closing the Frame via the OS-provided close button none of the Frames are unloaded/deleted, the "probable leaks"-screen says the following after and closing the frame 11 times:
11 instances of "LeakingInterface", loaded by "sun.misc.Launcher$AppClassLoader @ 0x6c00f08b8" occupy 393.71 MB (97,10%) bytes.
These instances are referenced from one instance of "java.lang.Thread", loaded by "<system class loader>"
Weirdly enough closing screens via the save changes button unloads some of the JFrames and the "probable leaks"-screen gives me different answers. Again for 11 Frames loaded and closed:
One instance of "LeakingInterface" loaded by "sun.misc.Launcher$AppClassLoader @ 0x6c01bc1f8" occupies 36.21 MB (19,20%) bytes. The memory is accumulated in one instance of "LeakingInterface" loaded by "sun.misc.Launcher$AppClassLoader @ 0x6c01bc1f8".
This message shows up 5 times, but only 5 times and the occupied memory is much smaller than closing by OS exit button.
I'm at the end of my wits now what causes this and how I can fix it. The debugging was done on Mac OS but the same behaviour (Memory Leak) can be seen on Windows as well.
Things I have tried that produced no result:
1) Converting "LeakingInterface" into a singleton class like this:
public static LeakingInterface showCardChangeInterface(){
if(instance != null){
instance.dispose();
instance = null;
System.gc(); //Trying to make sure the gc runs at least once
}
instance = new LeakingInterface();
return instance;
}
2) Instead of using just a constructor to show the Frame instead convert the frame into a field and making sure it is properly nulled and disposed before re-opening:
startButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(leakingFrame != null){
leakingFrame.dispose();
}
leakingFrame = new LeakingFrame();
}
});
3) Trying to replicate at least some of the frames properly disposing I tried to overload the OS Button to mirror the behaviour of the saveChanges button.
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
saveChanges();
dispose();
}
});
I'm very confused how this can provide different results.
E: As suggested here is a minimal example that can reproduce this behavior:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import javax.swing.JButton;
import javax.swing.JFrame;
public class LeakingFrame extends JFrame{
public static LeakingFrame instance;
private ArrayList<String> content;
public static void main(String[] args) {
JFrame testFrame = new JFrame("test");
testFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton startButton = new JButton("Open LeakingFrame");
startButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
LeakingFrame.showInstance();
}
});
testFrame.add(startButton);
testFrame.pack();
testFrame.setVisible(true);
}
public static LeakingFrame showInstance(){
if(instance!=null){
instance.dispose();
instance = null;
System.gc();
}
instance = new LeakingFrame();
return instance;
}
private LeakingFrame(){
super("LeakingFrame");
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
dispose();
}
});
content = new ArrayList<String>();
for(int i=0;i<150000;i++){
content.add("LARGESTRING"); //So it takes up at least some amount of memory
}
JButton dispose = new JButton("Dispose");
dispose.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dispose();
}
});
add(dispose);
pack();
setVisible(true);
}
}