3

I have an application that needs to open multiple JFrames (it's a log viewer, and sometimes you need to see a bunch of logs in separate windows to compare).

It appears that the JVM (Java 8 update 101 on OS X) is holding a strong reference to the JFrame, which is preventing it from being garbage collected, and eventually leads to an OutOfMemoryError being thrown.

To see the problem, run this problem with a max heap size of 200 megabytes. Each time a window is opened, it consumes 50 megabytes of RAM. Open three windows (using 150 megabytes of RAM). Then close the three windows (which calls dispose), which should free up memory. Then try to open a fourth window. An OutOfMemoryError is thrown and the fourth window does not open.

I've seen other answers stating that memory will be automatically released when necessary to avoid running out, but that doesn't seem to be happening.

package com.prosc.swing;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.NumberFormat;

public class WindowLeakTest {
    public static void main(String[] args) {
        EventQueue.invokeLater( new Runnable() {
            public void run() {
                JFrame launcherWindow = new JFrame( "Launcher window" );
                JButton launcherButton = new JButton( "Open new JFrame" );
                launcherButton.addActionListener( new ActionListener() {
                    public void actionPerformed( ActionEvent e ) {
                        JFrame subFrame = new JFrame( "Sub frame" ) {
                            private byte[] bigMemoryChunk = new byte[ 50 * 1024 * 1024 ]; //50 megabytes of memory

                            protected void finalize() throws Throwable {
                                System.out.println("Finalizing window (Never called until after OutOfMemory is thrown)");
                                super.finalize();
                            }
                        };
                        subFrame.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE );
                        subFrame.add( new JLabel( "Nothing to see here" ) );
                        subFrame.pack();
                        subFrame.setVisible( true );
                        System.out.println( "Memory usage after new window: " + getMemoryInfo() );
                    }
                } );
                launcherWindow.add( launcherButton );
                launcherWindow.pack();
                launcherWindow.setVisible( true );

                new Timer( 5000, new ActionListener() {
                    public void actionPerformed( ActionEvent e ) {
                        System.gc();
                        System.out.println( "Current memory usage after garbage collection: " + getMemoryInfo() );
                    }
                } ).start();
            }
        } );
    }

    public static String getMemoryInfo() {
        NumberFormat numberFormat = NumberFormat.getNumberInstance();
        return "Max heap size is " + numberFormat.format( Runtime.getRuntime().maxMemory() ) + "; free memory is " + numberFormat.format( Runtime.getRuntime().freeMemory() ) + "; total memory is " + numberFormat.format( Runtime.getRuntime().totalMemory() );
    }
}
Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
Jesse Barnum
  • 6,507
  • 6
  • 40
  • 69
  • Javadoc Java SE 7 Window.dispose: "The Window and its subcomponents can be made displayable again by rebuilding the native resources with a subsequent call to pack or show." This might be the reason for a reference still being present. – mm759 Sep 11 '16 at 15:19
  • Possibly duplicate: http://www.stackoverflow.com/questions/7376993/does-disposing-a-jframe-cause-memory-leakage. Do you agree? – mm759 Sep 11 '16 at 15:22
  • They are pretty much the same question - I didn't see a satisfactory answer though. I need multiple windows, and it seems like a bug that the developer is responsible for clearing out the contents of a window after dispose() is called on it. I was hoping that a code sample might elicit a better answer. – Jesse Barnum Sep 11 '16 at 16:01
  • Possible duplicate of [*Remove Top-Level Container on Runtime*](http://stackoverflow.com/q/6309407/230513); [profile](http://stackoverflow.com/q/2064427/230513) your actual code to find trouble spots. – trashgod Sep 11 '16 at 16:05
  • @trashgod: I used a profiler before posting the question, and found that some root-level native object is holding a reference to my JFrames. I'm trying to find out why this is happening and whether it's something I can control. Calling dispose() is clearly not sufficient. – Jesse Barnum Sep 11 '16 at 16:18
  • Been a few years (forgive my recollection of the details) but I recall observing something similar - manually removing all components from the `JFrame` recursively (from the JFrame and from themselves) seemed to help – copeg Sep 11 '16 at 16:41

1 Answers1

5

As shown here, there is an irreducible leak due to unrecoverable allocations related to a typical host peer component. The remnant is ~2 MB in the course of creating and disposing ~103 windows. In your case, the dominant leak is due to retained instances of bigMemoryChunk. One approach is to make the instances unreachable in a WindowListener.

this.addWindowListener(new WindowAdapter() {

    @Override
    public void windowClosing(WindowEvent e) {
        bigMemoryChunk = null;
    }
});

Why do we need to set bigMemoryChunk = null?

JFrame has no direct way to know that that each instance in your program has an associated instance of bigMemoryChunk. Such an object becomes eligible for garbage collection when it is unrechable; bigMemoryChunk is the only reference to the array object in this case, so setting it to null makes it immediately eligible for later garbage collection.

If the JFrame is the only thing holding a reference to bigMemoryChunk…then why don't the JFrame and bigMemoryChunk…both get garbage collected after the window has been disposed?

You may be confusing containment with inheritance and composition. The JFrame isn't "holding a reference to bigMemoryChunk;" the JFrame has an instance variable named bigMemoryChunk that holds a reference to an array object. The small amount of memory lost to the frame's peer is owned and managed by the host. The large amount of memory in bigMemoryChunk is your program's responsibility. The enclosed WindowListener allows you to associate management of the array object with closing the frame.

The profile below shows a series of four subframes opened; each one is then closed, followed by a forced garbage collection in the profiler.

profile

As profiled:

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.NumberFormat;

public class WindowLeakTest {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame launcherWindow = new JFrame("Launcher window");
                launcherWindow.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                JButton launcherButton = new JButton("Open new JFrame");
                launcherButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        JFrame subFrame = new JFrame("Sub frame") {
                            private byte[] bigMemoryChunk = new byte[50 * 1024 * 1024];

                            {
                                this.addWindowListener(new WindowAdapter() {

                                    @Override
                                    public void windowClosing(WindowEvent e) {
                                        bigMemoryChunk = null;
                                    }
                                });
                            }

                            @Override
                            protected void finalize() throws Throwable {
                                super.finalize();
                                System.out.println("Finalizing window.");
                            }
                        };
                        subFrame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                        subFrame.add(new JLabel("Nothing to see here"));
                        subFrame.pack();
                        subFrame.setVisible(true);
                    }
                });
                launcherWindow.add(launcherButton);
                launcherWindow.pack();
                launcherWindow.setVisible(true);
            }
        });
    }
}
Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • This definitely solves the problem, but should it be necessary? Shouldn't the JVM release the reference to the JFrame after dispose() has been called and when the application code is not holding any references to it? – Jesse Barnum Sep 11 '16 at 18:15
  • @JesseBarnum: In large part, it does just that; the small remnant [cited](http://stackoverflow.com/a/6310284/230513) is a few MB in the course of ~10^3 windows. Of course, you have to manage retention of application associated data yourself; see also [*What is the difference between a soft reference and a weak reference in Java?*](http://stackoverflow.com/q/299659/230513) – trashgod Sep 11 '16 at 22:38
  • why do we need to set bigMemoryChunk= null, it is an outgoing reference from JFrame? please explain. – hunter Sep 12 '16 at 03:45
  • `JFrame` has no knowledge of `bigMemoryChunk`; I've elaborated above. – trashgod Sep 12 '16 at 08:24
  • bigMemoryChunk, in my test, is designed to simulate the actual contents of the JFrame. Since the JFrame holds a reference to all of its children, and since they may in turn hold references to large amounts of memory, that's why I included it in the test. – Jesse Barnum Sep 12 '16 at 20:32
  • I didn't state my question very well: If the JFrame is the only thing holding a reference to bigMemoryChunk, and if nothing is holding a reference to the JFrame, then why don't the JFrame and bigMemoryChunk that it reference both get garbage collected after the window has been disposed? – Jesse Barnum Sep 12 '16 at 20:33
  • I understand that `bigMemoryChunk` is a placeholder; the exact formulation of your `WindowListener` depends on your application's actual data model; more above. – trashgod Sep 12 '16 at 23:07
  • 1
    @trashgod I don't understand what you're saying. According to my profiler, the only thing holding a reference to the byte array is the JFrame subclass. The only thing holding a reference to the JFrame subclass is sun.lwawt.macosx.CPlatformWindow. The only thing holding a reference to that is a local variable in java.lang.Thread. My question boils down to: Why is there a local variable in Thread that is still holding this CPlatformWindow reference? After I dispose the window, shouldn't all references to it be released, allowing it (and by extension, the big byte array) to be freed? – Jesse Barnum Sep 13 '16 at 02:58
  • Nope; the only thing holding a reference to the byte array is the variable named `bigMemoryChunk`. – trashgod Sep 13 '16 at 03:06
  • 1
    I give up, it appears that you are deliberately misunderstanding the question. – Jesse Barnum Sep 13 '16 at 03:10
  • The memory occupied by the array cannot be freed until the variable `bigMemoryChunk` no longer references it. Sorry I am unable to evince a more compelling explanation; it is not deliberate. – trashgod Sep 13 '16 at 03:15
  • OK. I still don't understand your answer, but I'm sorry for getting snippy. – Jesse Barnum Sep 13 '16 at 03:19
  • I empathize with your frustration; this is a design problem without a convenient, general solution; take some time to experiment. – trashgod Sep 13 '16 at 03:32
  • I'm having a similar problem. Sounds like the solution (or rather, work around) is that anytime a JFrame is closed, we (as application developers) have to know if the JFrame is referenced anywhere outside the thread local -> CPlatformWindow -> JFrame path and manually remove all components from the JFrame and nullify any variables (say if we extended the JFrame). I wish this CPlatformWindow had a WeakReference to the JFrame so that we do not have to do this, as there is no way to reach the JFrame in the example in the OP (short of iterating over thread locals, but who would do that!?). – Jason Aug 09 '17 at 17:19
  • @Jason: I typically use a single `JFrame` and dialogs as required. – trashgod Aug 09 '17 at 22:40