Java对话框中的垃圾收集

7
我现在遇到了一个非常奇怪的Java GC问题,当我尝试在一个JFrame中创建一个按钮时,点击按钮会显示一个需要处理并显示一些图片、需要近200M内存的JDialog。但问题是,当我关闭对话框并重新打开它时,有时会导致java.lang.OutOfMemoryError错误(并非每次都出现)。
为了解决这个问题,我简化了这个问题并进行了一些实验,结果让我更加困惑。我的“实验”中使用的代码如下所示:当我在一个框架中点击一个按钮时,我为一个整数数组分配了160M内存,并显示了一个对话框。但是,如果我关闭对话框并重新打开它,就会出现OutOfMemoryError错误。我调整了代码,结果如下:
  1. If I don’t create the dialog and show it, no memory problem.
  2. If I add a windowsCloseListener which invoke System.gc() to the dialog, no memory problem.
  3. If I invoke System.gc() in the run() method, memory problem shows.

    public class TestController {
      int[] tmp;
    
      class TDialog extends JDialog {
        public TDialog() {
          super();
          this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
          // If I uncommment this code, OutOfMemoryError seems to dispear in this situation
          // But I'm sure it not a acceptable solution
          /*
          this.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
              System.out.println("windowsclose");
              TDialog.this.dispose();
              System.gc();
            }
          });
          */
        }
      }
    
      TDialog dia;
    
      public void run() {
        // If I do System.gc() here, OutOfMemoryError still exist
        // System.gc();
        tmp = new int[40000000];
        for (int i = 0; i < tmp.length; i += 10)
          tmp[i] = new Random().nextInt();
    
        dia = new TDialog();
        dia.setVisible(true);
      }
    
      public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
          @Override
          public void run() {
            final JFrame frame = new JFrame("test");
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.setLocationRelativeTo(null);
            frame.setSize(200, 200);
    
            JButton button = new JButton("button");
            button.addActionListener(new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                TestController controller = new TestController();
                controller.run();
                controller = null;
              }
            });
    
            frame.add(button);
            frame.setVisible(true);
          }
        });
      }
    }
    
我读了很多关于Java GC工作原理的文章。我认为,如果Java尝试在堆中分配一些空间,但没有足够的自由空间,Java将进行垃圾回收。如果通过“GC图”无法从GC根访问对象,则表示从u到v的边缘表示u对v有引用,根是线程工作堆栈或本地资源中的某些内容。这个对象是无用的,可以被Java的GC收集。
现在问题是,当我点击按钮并尝试创建一个整数数组时,我上次创建的整数数组肯定可以被Java的GC收集。那么为什么会出错呢?
另外,我用来启动JVM的参数是“java -Xmx256m”。

感谢您在调试中的努力,感谢您创建了一个SSCCE,并且为解决问题付出了努力。我在工作中没有时间运行或调试您的程序,但我想知道您是否遇到了由监听器引起的持久软引用问题。 - Hovercraft Full Of Eels
如果将对话框设置为模态,然后在调用对话框的代码中通过调用dispose()方法来释放对话框,这样会怎么样呢? - Hovercraft Full Of Eels
2个回答

3
您在tmp仍然持有上一个int [40000000]的引用之前分配了new int[40000000]。像tmp = new int [40000]这样的表达式中的操作顺序是:
  1. new int[40000]
  2. 将数组的引用分配给tmp
所以在1.中,tmp仍然保持着它的上一个值的引用。
尝试执行以下操作:
tmp = null;
tmp = new int[40000000];

在将tmp设置为null后运行System.gc()可能会有所帮助,因为这正是您希望JVM在此时执行的操作。请注意,JVM不需要使用System.gc()运行完整的垃圾回收,但在这种情况下它可能会这样做。 - Chill
谢谢你们两位的建议,非常有帮助。但实际上,每次我创建一个新的TestController对象并使用它的tmp[]数组时,它们都没有相同的引用。此外,我尝试在tmp = new int [xxx]之前将tmp = null,但它不起作用,仍然会导致java.lang.OutOfMemoryError错误。 - ryanaaa

2

试试这个:

import java.awt.*;
import java.awt.event.*;
import java.util.Random;
import javax.swing.*;

public class TestController {
   private JFrame frame;
   int[] tmp;

   public TestController(JFrame frame) {
      this.frame = frame;
   }

   public void finish() {
      if (dia != null) {
         dia.dispose();
      }
      tmp = null;
   }

   class TDialog extends JDialog {
      public TDialog() {
         super(frame, "Dialog", true);
         this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
      }
   }

   TDialog dia;

   public void run() {
      tmp = new int[40000000];
      for (int i = 0; i < tmp.length; i += 10)
         tmp[i] = new Random().nextInt();
      dia = new TDialog();
      dia.setVisible(true);
   }

   public static void main(String[] args) {
      EventQueue.invokeLater(new Runnable() {
         @Override
         public void run() {
            final JFrame frame = new JFrame("test");
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.setLocationRelativeTo(null);
            frame.setSize(200, 200);
            JButton button = new JButton("button");
            button.addActionListener(new ActionListener() {
               @Override
               public void actionPerformed(ActionEvent e) {
                  TestController controller = new TestController(frame);
                  controller.run();
                  // controller = null;
                  System.out.println("here");
                  controller.finish();
               }
            });
            frame.add(button);
            frame.setVisible(true);
         }
      });
   }
}

finish()方法中,您需要清除对话框及其数据。如果对话框不是模态对话框,则需要使用WindowListener。


您在评论中提到:

但您能告诉我我的代码有什么问题吗?“模态”是什么意思?我已经在java文档中阅读了Dialog的setModal方法,它的意思是“当显示对话框时是否阻止其他窗口输入”,似乎与您所说的不同。

事实上,模态对话框会阻止来自调用窗口的输入,并且一旦对话框可见,代码流从调用代码处冻结。一旦对话框不再可见,代码就会继续运行。

对于您的对话框的问题,没有神奇的解决方案,但是它允许我们知道何时对话框不再可见 - 代码从对话框设置为可见的位置恢复,因此允许我们在此时调用清理代码。在这里,我调用了finish()方法。

如果您不想让对话框是模态的,则需要使用WindowListener,并侦听对话框关闭,然后在那里调用完成方法。

我的所有代码做的就是确保int数组在创建新的int数组之前可以进行垃圾回收。


谢谢你的帮助!你的代码有效,但是你能告诉我我的代码哪里出了问题吗?还有“模态”的意思是什么?我已经阅读了Java文档中Dialog的setModal方法的API,它的意思是“当显示对话框时是否阻止其他窗口的输入”,似乎不是你所指的那个意思。 - ryanaaa
再唱同一首歌,重复使用,重复使用或删除+1。 - mKorbel
图像、向量等对象永远不会被finalize,因此GC对它们没有任何兴趣,因为它们的父级顶层容器在API中没有finalize()方法,因此它们的子级永远不会被GC清理,但引用已经为空。 - mKorbel
@user2328588 这里是解决方案 - mKorbel
即使java的默认finalize方法什么也不做,我使用的tmp[]数组在关闭对话框后就没有被引用了,它的内存可以通过GC释放。当我关闭对话框并重新打开另一个对话框时,jvm应该能够释放上次使用的tmp[]数组的内存,并分配一个新的数组。OutOfMemoryError不应该发生,我是正确的吗?此外,如果我在jdk/bin中使用jvisualvm.exe,并使用它请求jvm进行垃圾收集,那么该程序使用的内存确实从150M减少到了近乎0M。 - ryanaaa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接