JFrame is never garbage collected

2019-08-02 04:09发布

问题:

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() );
    }
}

回答1:

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.

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);
            }
        });
    }
}