Java 如何防止JTextPane.setCaretPosition(int)中的内存泄漏?

Java 如何防止JTextPane.setCaretPosition(int)中的内存泄漏?,java,swing,memory-leaks,jtextpane,caret,Java,Swing,Memory Leaks,Jtextpane,Caret,我正在使用基于Swing的GUI开发Java应用程序。应用程序使用JTextPane输出日志消息,如下所示:1)截断现有文本,使总文本大小保持在限制范围内;2) 添加新文本;3) 滚动到末尾(实际逻辑略有不同,但此处不相关) 我使用Eclipse和JVM监视器来确定合理的文本大小限制,并发现了严重的内存泄漏。我试图从基础文档中删除UndoableEditListeners并禁用自动插入符号位置更新(通过使用DefaultCaret.NEVER\u UPDATE和JTextPane.setCare

我正在使用基于Swing的GUI开发Java应用程序。应用程序使用
JTextPane
输出日志消息,如下所示:1)截断现有文本,使总文本大小保持在限制范围内;2) 添加新文本;3) 滚动到末尾(实际逻辑略有不同,但此处不相关)

我使用Eclipse和JVM监视器来确定合理的文本大小限制,并发现了严重的内存泄漏。我试图从基础文档中删除
UndoableEditListener
s并禁用自动插入符号位置更新(通过使用
DefaultCaret.NEVER\u UPDATE
JTextPane.setCaretPosition(int)
显式更改位置),但没有成功。最后,我决定完全禁用更改插入符号的位置,这修复了泄漏

我有两个问题:

  • 我的代码有问题吗?如果是,我如何更改它以完成任务

  • 这是一个Swing/JVM错误吗?如果是,我如何报告

  • 细节: 这里是SSCCE:GUI,带有文本窗格和两个按钮,用于小型和压力测试
    FIX
    FIXXX
    标志对应于我修复内存泄漏的尝试

    package memleak;
    
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.event.UndoableEditListener;
    import javax.swing.text.*;
    
    class TestMain
    {
      private JTextPane textPane;
      // try to fix memory leak
      private static final boolean FIX = false;
      // disable caret updates completely
      private static final boolean FIXXX = false;
      // number of strings to append
      private static final int ITER_SMALL = 20;
      private static final int ITER_HUGE = 1000000;
      // limit textPane content
      private static final int TEXT_SIZE_MAX = 100;
    
      TestMain()
      {
        JFrame frame = new JFrame();
        JPanel panel = new JPanel();
        textPane = new JTextPane();
        textPane.setEditable(false);
        if (FIX)
        {
          tryToFixMemory();
        } // end if FIX
        JScrollPane scrollPane = new JScrollPane(textPane);
        scrollPane.setPreferredSize(new Dimension(100, 100) );
        panel.add(scrollPane);
        JButton buttonSmall = new JButton("small test");
        buttonSmall.addActionListener(new ButtonHandler(ITER_SMALL) );
        panel.add(buttonSmall);
        JButton buttonStress = new JButton("stress test");
        buttonStress.addActionListener(new ButtonHandler(ITER_HUGE) );
        panel.add(buttonStress);
        frame.add(panel);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
      } // end constructor
    
      public static void main(String[] args)
      {
        @SuppressWarnings("unused")
        TestMain testMain = new TestMain();
      } // end main
    
      private void append(String s)
      {
        Document doc = textPane.getDocument();
        try
        {
          int extraLength = doc.getLength() + s.length() - TEXT_SIZE_MAX;
          if (extraLength > 0)
          {
            doc.remove(0, extraLength);
          } // end if extraLength
          doc.insertString(doc.getLength(), s, null);
          if (FIX && !FIXXX)
          {  // MEMORY LEAK HERE
            textPane.setCaretPosition(doc.getLength() );
          } // end if FIX
        }
        catch (Exception e)
        {
          e.printStackTrace();
          System.exit(1);
        } // end try
      } // end method append
    
      private void tryToFixMemory()
      {    
        // disable caret updates
        Caret caret = textPane.getCaret();
        if (caret instanceof DefaultCaret)
        {
          ( (DefaultCaret) caret).setUpdatePolicy(
              DefaultCaret.NEVER_UPDATE);
        } // end if DefaultCaret
    
        // remove registered UndoableEditListeners if any
        Document doc = textPane.getDocument();
        if (doc instanceof AbstractDocument)
        {
          UndoableEditListener[] undoListeners = 
              ( (AbstractDocument) doc).getUndoableEditListeners();
          if (undoListeners.length > 0)
          {
            for (UndoableEditListener undoListener : undoListeners)
            {
              doc.removeUndoableEditListener(undoListener);
            } // end for undoListener
          } // end if undoListeners
        } // end if AbstractDocument
      } // end method tryToFixMemory
    
      private class ButtonHandler implements ActionListener
      {
        private final int iter;
    
        ButtonHandler(int iter)
        {
          this.iter = iter;
        } // end constructor
    
        @Override
        public void actionPerformed(ActionEvent e)
        {
          for (int i = 0; i < iter; i++)
          {
            append(String.format("%10d\n", i) );
          } // end for i
        } // end method actionPerformed
    
      } // end class ButtonHandler
    
    } // end class TestMain
    
    详细的内存统计信息显示
    java.awt.event.InvocationEvent
    sun.awt.EventQueueItem
    、和
    javax.swing.text.DefaultCaret$1
    的计数非常高(在固定版本中不存在):

    设置
    FIX=true
    并没有改善这种情况

    两个标志均为
    : 小测验 现在显示插入符号位置未更新:

    压力测试 工作正常且无内存泄漏迹象:


    原因是您正在事件分派线程中运行for循环(请参阅)。这是发生所有用户界面交互的线程

    如果您正在运行一个长任务,那么您应该在另一个线程中运行它,以便用户界面保持响应。如果需要在用户界面上进行更改,例如更改JTextPane中的文本并将插入符号位置设置为事件分派线程以外的其他线程,则需要调用
    EventQueue.invokeLater()
    EventQueue.invokeAndWait()
    (请参阅)

    我认为设置插入符号位置触发的事件在您的案例中是排队的,并且只能在循环完成时处理(因为这两个事件都是在事件分派线程中处理的)。所以你应该试试这样:

    @Override
    public void actionPerformed(ActionEvent e)
    {
      new Thread(new Runnable() {
        @Override
        public void run() {
          for (int i = 0; i < iter; i++)
          {
            final String display = String.format("%10d\n", i);
            try {
              EventQueue.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                  append(display);
                }
              });
            } catch (Exception e) {
              e.printStackTrace();
            }
          } // end for i
        }
      }).start();
    } 
    
    @覆盖
    已执行的公共无效操作(操作事件e)
    {
    新线程(newrunnable()){
    @凌驾
    公开募捐{
    对于(int i=0;i

    如果您只在x次迭代后调用
    EventQueue.invokeAndWait
    并缓存以前需要显示的结果,可能会更好。

    另请参见。@Andrey您是否找到了导致它的原因?我的一个使用HTMLEditorKit的应用程序也存在同样的问题,我无法找到修复内存泄漏的方法。下面是一个我从未得到答案的例子:@M.H.是的,请检查下面的答案。简言之,在事件处理程序中追加字符串,然后更新插入符号位置将触发另一个事件,该事件将一直排队,直到处理完所有字符串。如果这个队列很小,那么就没有问题,否则JVM就会被卡住,因为没有内存来处理事件。附加1个字符的10000个字符串和一个10000个字符的字符串之间有很大的区别。@andrey我真的不知道你的意思。实际上,我看过你的代码,但我不知道如何修复它。你看过我的例子吗?在那里,我实际上是一个线程,所以怎么可能有一个队列?排什么队?我还将文本窗格设置为noteditable,并且从不更改插入符号。我也不会通过htmleditorkit附加我使用insertBeforeEnd的字符串。@M.H.您提供的链接给了我404,因此我无法对此发表评论。在我的例子中,解决方案是构建一个长
    字符串
    ,然后立即附加它,而不是在循环中,因此我避免多次调用
    setCaretPosition()
    。我不确定这是否可行。你试过这个密码吗?应该注意的是,我使用带有
    String.format()
    的循环只是为了模拟真实数据,对这一部分的优化毫无价值。无论如何,问题在于
    textPane.setCaretPosition(doc.getLength()),不带字符串。我本来打算接受您的答案,但您的代码无法编译。我没有足够的声誉编辑它,只是建议编辑。然而,我的编辑是错误的。你能自己更新代码吗?@sfrutig我不这么认为。我的一个使用HTMLEditorKit的应用程序也存在同样的问题,我无法找到修复内存泄漏的方法。下面是一个我从未得到答案的例子:
    
    @Override
    public void actionPerformed(ActionEvent e)
    {
      new Thread(new Runnable() {
        @Override
        public void run() {
          for (int i = 0; i < iter; i++)
          {
            final String display = String.format("%10d\n", i);
            try {
              EventQueue.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                  append(display);
                }
              });
            } catch (Exception e) {
              e.printStackTrace();
            }
          } // end for i
        }
      }).start();
    }