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

Posted

技术标签:

【中文标题】如何防止 JTextPane.setCaretPosition(int) 中的内存泄漏?【英文标题】:How to prevent memory leak in JTextPane.setCaretPosition(int)? 【发布时间】:2015-09-03 08:42:03 【问题描述】:

我正在使用基于 Swing 的 GUI 开发 Java 应用程序。应用程序使用JTextPane 输出日志消息如下: 1) 截断现有文本以保持总文本大小在限制之下; 2) 追加新文本; 3) 滚动到最后(实际逻辑略有不同,但这里无关)。

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

我有两个问题:

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

    这是 Swing/JVM 错误吗?如果是,我该如何举报?

详情:

这里是 SSCCE:带有 textPane 和两个按钮的 GUI,用于小型和压力测试。 FIXFIXXX 标志对应于我修复内存泄漏的尝试。

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

JVM 来自官方 Oracle Java SE Development Kit 8u45 for Linux x64。所有测试均使用-Xmx100m 限制完成。

两个标志都是false:

小测试

按预期工作:

压力测试

GUI 在中间点冻结:

内存泄漏:

在某些时候没有剩余内存,我收到以下错误:

Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: GC overhead limit exceeded
  at java.util.Formatter.parse(Formatter.java:2560)
  at java.util.Formatter.format(Formatter.java:2501)
  at java.util.Formatter.format(Formatter.java:2455)
  at java.lang.String.format(String.java:2928)
  at memleak.TestMain$ButtonHandler.actionPerformed(TestMain.java:117)
  at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:2022)
  at javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2346)
  at javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:402)
  at javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:259)
  at javax.swing.plaf.basic.BasicButtonListener.mouseReleased(BasicButtonListener.java:252)
  at java.awt.Component.processMouseEvent(Component.java:6525)
  at javax.swing.JComponent.processMouseEvent(JComponent.java:3324)
  at java.awt.Component.processEvent(Component.java:6290)
  at java.awt.Container.processEvent(Container.java:2234)
  at java.awt.Component.dispatchEventImpl(Component.java:4881)
  at java.awt.Container.dispatchEventImpl(Container.java:2292)
  at java.awt.Component.dispatchEvent(Component.java:4703)
  at java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4898)
  at java.awt.LightweightDispatcher.processMouseEvent(Container.java:4533)
  at java.awt.LightweightDispatcher.dispatchEvent(Container.java:4462)
  at java.awt.Container.dispatchEventImpl(Container.java:2278)
  at java.awt.Window.dispatchEventImpl(Window.java:2750)
  at java.awt.Component.dispatchEvent(Component.java:4703)
  at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:758)
  at java.awt.EventQueue.access$500(EventQueue.java:97)
  at java.awt.EventQueue$3.run(EventQueue.java:709)
  at java.awt.EventQueue$3.run(EventQueue.java:703)
  at java.security.AccessController.doPrivileged(Native Method)
  at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:75)
  at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:86)
  at java.awt.EventQueue$4.run(EventQueue.java:731)
  at java.awt.EventQueue$4.run(EventQueue.java:729)
Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: GC overhead limit exceeded
  at javax.swing.text.Glyphpainter1.modelToView(GlyphPainter1.java:147)
  at javax.swing.text.GlyphView.modelToView(GlyphView.java:653)
  at javax.swing.text.CompositeView.modelToView(CompositeView.java:265)
  at javax.swing.text.BoxView.modelToView(BoxView.java:484)
  at javax.swing.text.ParagraphView$Row.modelToView(ParagraphView.java:900)
  at javax.swing.text.CompositeView.modelToView(CompositeView.java:265)
  at javax.swing.text.BoxView.modelToView(BoxView.java:484)
  at javax.swing.text.CompositeView.modelToView(CompositeView.java:265)
  at javax.swing.text.BoxView.modelToView(BoxView.java:484)
  at javax.swing.plaf.basic.BasicTextUI$RootView.modelToView(BasicTextUI.java:1509)
  at javax.swing.plaf.basic.BasicTextUI.modelToView(BasicTextUI.java:1047)
  at javax.swing.text.DefaultCaret.repaintNewCaret(DefaultCaret.java:1308)
  at javax.swing.text.DefaultCaret$1.run(DefaultCaret.java:1287)
  at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
  at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
  at java.awt.EventQueue.access$500(EventQueue.java:97)
  at java.awt.EventQueue$3.run(EventQueue.java:709)
  at java.awt.EventQueue$3.run(EventQueue.java:703)
  at java.security.AccessController.doPrivileged(Native Method)
  at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:75)
  at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
  at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
  at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
  at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
  at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
  at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
  at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
Exception in thread "RMI TCP Connection(idle)" java.lang.OutOfMemoryError: GC overhead limit exceeded
  at java.lang.Thread.getName(Thread.java:1135)
  at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:677)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  at java.lang.Thread.run(Thread.java:745)

详细的内存统计显示java.awt.event.InvocationEventsun.awt.EventQueueItemjavax.swing.text.DefaultCaret$1 的计数非常高(固定版本中没有):

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

两个标志都是true:

小测试

现在显示插入符号位置未更新:

压力测试

有效且没有内存泄漏的迹象:

【问题讨论】:

另见Initial Threads @Andrey 你有没有发现是什么原因造成的?我在我的一个带有 htmlEditorKit 的应用程序中遇到了同样的问题,我不知道如何修复内存泄漏。这是一个我从未得到答案的例子:***.com/questions/43711973/… @M.H.是的,请检查下面的答案。简而言之,在事件处理程序中附加字符串,然后更新插入符号位置会触发另一个排队的事件,直到处理完所有字符串。如果这个队列很小,那么就没有问题,否则 JVM 会因为没有剩余的内存来处理事件而被卡住。追加1个字符的10000个字符串和1个10000个字符的字符串是有很大区别的。 @andrey 我真的不知道你的意思是什么,我看过你的代码,但我不知道如何解决它。你看过我的例子吗?那里我实际上是一个线程,所以怎么会有一个队列?什么排队?我还将文本窗格设置为可编辑,并且我从不更改插入符号。我也没有通过 htmleditorkit 附加我使用 insertBeforeEnd 的字符串。 @M.H.您提供的链接给了我 404,所以我无法对此发表评论。就我而言,解决方案是构建一个 long String,然后一次追加,而不是在循环中,因此我避免多次调用 setCaretPosition() 【参考方案1】:

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

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

我认为设置插入符号位置的触发事件在您的情况下排队并且只能在循环完成时进行处理(因为两者都在事件调度线程中处理)。所以你应该尝试这样的事情:

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

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

【讨论】:

我不确定这是否可行。你试过这个代码吗?需要注意的是,我使用String.format()的循环只是为了模拟真实数据,这部分的优化是毫无价值的。无论如何,问题在于textPane.setCaretPosition(doc.getLength() );,而不是字符串。 我打算接受你的回答,但你的代码无法编译。我没有足够的声誉来编辑它,只是建议编辑。但是,我的编辑是rejected。你能自己更新代码吗? @sfrutig 我不这么认为。我在我的一个带有 HTMLEditorKit 的应用程序中遇到了同样的问题,我不知道如何修复内存泄漏。这是一个我从未得到答案的例子:***.com/questions/43711973/…

以上是关于如何防止 JTextPane.setCaretPosition(int) 中的内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止超卖

在winform当中提交数据,如何防止重复提交?

PostgreSQL如何防止表太大?

如何彻底防止SQL注入?

如何防止VBS的错误提示

https如何防止会话劫持