JTextPane / HTMLEditorKit memory leak

2019-07-29 05:33发布

I have the following issue with an app of mine, a basic IRC tool, which adds messages to a "JTextPane" with using "HTMLEditorKit" as an output GUI. I noticed, that randomly but over time, my app was using more and more memory, easily blowing up in crowded channels to already 300MB after just about 20 minutes of usage. I think the problem is somehow related to "JTextPane", because I can reproduce the issue with this code:

package javaapplication26;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Element;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;

public class NewJFrame extends javax.swing.JFrame {

    private long globalCount = 0;

    /**
     * Creates new form NewJFrame
     */
    public NewJFrame() {

        initComponents();

        this.setSize(500, 200);
        this.setLocationRelativeTo(null);

        this.jTextPane1.setEditorKit(new HTMLEditorKit());
        this.jTextPane1.setContentType("text/html");

        this.jTextPane1.setText("<html><body><div id=\"GLOBALDIV\"></div></body></html>");

        this.jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        this.jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);

        DefaultCaret caret = (DefaultCaret) this.jTextPane1.getCaret();
        caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

        this.jScrollPane1.setAutoscrolls(false);
        this.jTextPane1.setAutoscrolls(false);

        Thread fillThread = new Thread() {

            @Override
            public void run() {

                while (!interrupted()) {

                    try {

                        removeFromPane(jTextPane1);
                        insertHTMLToPane(jTextPane1, "<div>"+globalCount+"</div>");
                        Thread.sleep(1);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(NewJFrame.class.getName()).log(Level.SEVERE, null, ex);
                        break;
                    }
                }
            }
        };

        fillThread.start();
    }

    private void removeFromPane(JTextPane pane) {

        HTMLDocument doc = (HTMLDocument) pane.getDocument();
        Element element = doc.getElement("ID" + (this.globalCount - 10));

        if (element != null) {
            doc.removeElement(element);
        }
    }

    private void insertHTMLToPane(JTextPane pane, String htmlCode) {

        this.globalCount++;

        HTMLDocument doc = (HTMLDocument) pane.getDocument();

        Element element = doc.getElement("GLOBALDIV");

        if (element != null) {

            try {
                doc.insertBeforeEnd(element, "<div id=\"ID"+this.globalCount+"\">" + htmlCode + "</div>");
            } catch (BadLocationException | IOException ex) {
                Logger.getLogger(NewJFrame.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        jPanel1 = new javax.swing.JPanel();
        jScrollPane1 = new javax.swing.JScrollPane();
        jTextPane1 = new javax.swing.JTextPane();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jPanel1.setLayout(new java.awt.BorderLayout());

        jScrollPane1.setViewportView(jTextPane1);

        jPanel1.add(jScrollPane1, java.awt.BorderLayout.CENTER);

        getContentPane().add(jPanel1, java.awt.BorderLayout.CENTER);

        pack();
    }// </editor-fold>                        

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new NewJFrame().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify                     
    private javax.swing.JPanel jPanel1;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTextPane jTextPane1;
    // End of variables declaration                   
}

The weird thing is, it doesnt happen with 100% chance when letting it run in Netbeans. Sometimes it stays around 70MB and never grows, but then running another time, it randomly explodes, and already grows to about 200-250MB after a minute or two.

I dont really know whats the data in memory growing more and more. It seems removing a line via "doc.removeElement(element)" doesnt always flags the object behind it to be cleared with next GC routine.

Letting it run in Netbeans with the profiler, I get something like this:enter image description here

It seems there is some kind of "undo mechanism" keeping reference to all inserted lines? I am no expert in using the profiler though because I am not getting some logic out of it, where things like char[] and some other growing into the thousands, even if nothing happens in the program.

This though seems to hint, that whatever reason for, the JTextPane creates for each insert a new StyleSheet and keeps it forever in a HashTable:enter image description here

I would welcome any help to find out why this is happening, or how to fix the issue. I am using latest 64bit JDK of course under Windows 10. Thank you very much

1条回答
我欲成王,谁敢阻挡
2楼-- · 2019-07-29 06:00

Your fillThread is really scary and non-sense, for event sake, please do not go that way.

This thread is kind of lets eat resources, and beside java itself is heavy enough, it could make a great mess in system indeed.

You may invoke you business(remove last message and add new one) based on an event, here you may rely on a button click or enter key over the textbox.

Even if you are working with a stateless stuff(like http), you may increase the sleep time to something more senseful, maybe 1 second, or even more.

One possible issue(as I experienced too) could be over-lazy GC. I cannot say it 100%, but since your thread is eating(heavy work), GC could not find a time to go for memory.
Even beside JVM indicates it has threaded-GC, but it may not work over the memory and release the data sometimes(as you experienced too) parallel.

You may also check the hashmap you stated, and make sure all data need to be added and removed are done as expected.

Also your globalCount is not thread safe, you may use AtomicInteger instead.

查看更多
登录 后发表回答