JTextPane / HTMLEditorKit memory leak
Asked Answered
N

1

1

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

Never answered 5/7, 2017 at 20:9 Comment(0)
G
0

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.

Ghee answered 5/7, 2017 at 20:42 Comment(6)
First, of course globalCount is thread safe ( #12826347 ). primitive variables in Java are always thread safe. Second, this is en example to show the memory leak... what does " non-sense" even mean... I dont have a "single" event, I have hundreds maybe more per second, I need to fill new lines for each event, and remove the first element, if element count is for example over 200.Never
I also dont know what you mean with "make sure all data need to be added and removed are done as expected" ... I dont even know what kind of HashMap that is ... ... that is the whole problem? It is some intern whatever map from the EditorKit storing whatever. It seems the remove and add doesnt really cut all lines to the object behind the lines.Never
@M.H. this is your whole code? You may please provide more real samples. The code you provide is not really logical, and if you get crash it's normal with that heavy endless loop.Ghee
o_0 no it is NOT normal. dont you understand the issue here? If I add remove add remove add remove one line, it shouldt explode. This is the most basic simplified example of the issue. I cant and wont provide a 100 classes "example" of my project. The globalcount may not be ts here yes, but it doesnt matter. It is added to at one time just so it doesnt matter.Never
Thread.sleep(1); means sleep for 1 millisecond, and the loop is kind of endless, since no one is there to interrupt it. So when you run 1000 text remove, and add new one in 1 second, then it could make troubles like crash. Beside how do you expect the UI refreshes all of these operations at time? @M.H.Ghee
JESUS : O Dont you get it!??? Than change it to 1000, and go to sleep 24h and come back, memory leak will be there anyway. It is just for demonstration reasons MAYBE!? Let me ask differently, maybe then you will understand: Whats the correct way to queue up events, and properly work them down on the gui thread so there is no memory leak happening from the jtextpane?Never

© 2022 - 2024 — McMap. All rights reserved.