【问题标题】:Inserting some characters in JTextPane causes performance problems and memory leak在 JTextPane 中插入一些字符会导致性能问题和内存泄漏
【发布时间】:2013-05-10 18:54:51
【问题描述】:

我的聊天客户端有一个插入文本的 JTextPane,每秒最多可以插入几行。它通常可以正常工作,即使是更长的时间(例如一个小时),但有时它会变得非常慢,使用大量 CPU 和内存,有时高达 1GB 并且几乎完全冻结。

我添加了“-Xrunhprof:heap=sites”参数以找出正在使用内存的内容,并且从我可以收集到的内容中,它与文本渲染有关,尽管我并不真正了解这些东西,所以这更像是一个有根据的猜测。这是结果的一部分,是在内存使用率异常高时拍摄的。我在每个条目下都包含了适当的跟踪。其他堆转储看起来略有不同,但它总是指向相同或相似的类(名称中带有 Glyph 的东西)。不知道如何正确解释这一点,以及它是否真的有助于解决这个问题。

         percent          live          alloc'ed  stack class
rank   self  accum     bytes objs     bytes  objs trace name
   1 16.33% 16.33%  11209120 350285  99416352 3106761 319103 java.awt.geom.Rectangle2D$Float

TRACE 319103:
java.awt.geom.RectangularShape.<init>(RectangularShape.java:56)
java.awt.geom.Rectangle2D.<init>(Rectangle2D.java:511)
java.awt.geom.Rectangle2D$Float.<init>(Rectangle2D.java:111)
sun.font.StandardGlyphVector$GlyphStrike.getGlyphOutlineBounds(StandardGlyphVector.java:1790)

   2 14.28% 30.61%   9799744 3958  52026864 49485 319095 float[]

TRACE 319095:
sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:851)
sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583)
sun.font.ExtendedTextSourceLabel.getCharinfo(ExtendedTextSourceLabel.java:509)
sun.font.ExtendedTextSourceLabel.getLineBreakIndex(ExtendedTextSourceLabel.java:455)

   3  8.17% 38.77%   5604560 350285  49708176 3106761 319110 sun.font.DelegatingShape

TRACE 319110:
sun.font.DelegatingShape.<init>(DelegatingShape.java:43)
sun.font.StandardGlyphVector.getGlyphVisualBounds(StandardGlyphVector.java:586)
sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:864)
sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583)

   4  7.96% 46.74%   5466576 9933  40683104 164341 319090 float[]

TRACE 319090:
sun.font.GlyphLayout$GVData.createGlyphVector(GlyphLayout.java:596)
sun.font.GlyphLayout.layout(GlyphLayout.java:476)
sun.font.ExtendedTextSourceLabel.createGV(ExtendedTextSourceLabel.java:325)
sun.font.ExtendedTextSourceLabel.getGV(ExtendedTextSourceLabel.java:311)

   5  4.07% 50.81%   2795304 9933  21434888 164341 319089 int[]

TRACE 319089:
sun.font.GlyphLayout$GVData.createGlyphVector(GlyphLayout.java:591)
sun.font.GlyphLayout.layout(GlyphLayout.java:476)
sun.font.ExtendedTextSourceLabel.createGV(ExtendedTextSourceLabel.java:325)
sun.font.ExtendedTextSourceLabel.getGV(ExtendedTextSourceLabel.java:311)

   6  3.71% 54.52%   2544072 106003 183421728 7642572 319087 java.awt.geom.Point2D$Float

TRACE 319087:
java.awt.geom.Point2D.<init>(Point2D.java:237)
java.awt.geom.Point2D$Float.<init>(Point2D.java:69)
sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:791)
sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:787)

   7  3.70% 58.22%   2539560 105815 182834016 7618084 319088 java.awt.geom.Point2D$Float

TRACE 319088:
java.awt.geom.Point2D.<init>(Point2D.java:237)
java.awt.geom.Point2D$Float.<init>(Point2D.java:69)
sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:809)
sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:787)

   8  2.20% 60.42%   1512888 6109  14728808 123309 319100 java.awt.Shape[]

TRACE 319100:
sun.font.StandardGlyphVector.getGlyphVisualBounds(StandardGlyphVector.java:580)
sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:864)
sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583)
sun.font.ExtendedTextSourceLabel.getCharinfo(ExtendedTextSourceLabel.java:509)

   9  2.20% 62.62%   1507120 2151  49362432 73824 319503 float[]

TRACE 319503:
sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:851)
sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583)
sun.font.ExtendedTextSourceLabel.getCharinfo(ExtendedTextSourceLabel.java:509)
sun.font.ExtendedTextSourceLabel.getCharX(ExtendedTextSourceLabel.java:353)

  10  2.09% 64.71%   1437120 44910  99416352 3106761 319111 java.awt.geom.Rectangle2D$Float

TRACE 319111:
java.awt.geom.RectangularShape.<init>(RectangularShape.java:56)
java.awt.geom.Rectangle2D.<init>(Rectangle2D.java:511)
java.awt.geom.Rectangle2D$Float.<init>(Rectangle2D.java:128)
java.awt.geom.Rectangle2D$Float.getBounds2D(Rectangle2D.java:251)

  11  1.84% 66.55%   1262456    6   1707160    18 307780 char[]

TRACE 307780:
javax.swing.text.GapContent.allocateArray(GapContent.java:94)
javax.swing.text.GapVector.resize(GapVector.java:214)
javax.swing.text.GapVector.shiftEnd(GapVector.java:229)
javax.swing.text.GapContent.shiftEnd(GapContent.java:345)

  12  1.16% 67.71%    794640 9933  13147280 164341 319092 sun.font.StandardGlyphVector

TRACE 319092:
    java.awt.font.GlyphVector.<init>(GlyphVector.java:109)
 sun.font.StandardGlyphVector.<init>(StandardGlyphVector.java:185)
    sun.font.GlyphLayout$GVData.createGlyphVector(GlyphLayout.java:607)
    sun.font.GlyphLayout.layout(GlyphLayout.java:476)

我还使用 JConsole 监控了该程序,并注意到当它开始使用更多资源时,聊天日志中有一些我不认识的字符(例如,表情符号、某种印度字符和某种泰语字符)被用作表情符号的一部分)。我自己尝试将相同的字符插入到 JTextPane 中,这本身花费了非常长的时间,并且还导致后续文本插入速度变慢了很多。

我创建了一个可以重现问题的 SSCCE:

  • 插入明显破坏某些东西的字符后..
    • ..如果没有插入更多的换行符,它会在几百行后变慢。
    • ..如果已经存在几百行,则在每次插入时更改已添加到 StyledDocument 的样式时,它会变慢很多。
    • ..否则它只会稍微慢一点(CPU 使用率增加几个百分点),但会逐渐使用越来越多的内存。

我猜不添加换行符会将所有插入的文本视为一个实体,而更改已添加到 StyledDocument 的样式可能会以某种方式更新整个文档,尽管我没有意识到这一点,因为它实际上并没有改变已插入文本的样式。

现在这里是 SSCCE(用 jdk1.7.0_21 测试),输入一个简单的命令:“test”添加了许多相同的行,“insert1”或“insert2”添加了一个减慢所有速度的字符,“style " 在更改已添加到 StyledDocument 的样式和另一个样式之间发生变化,"linebreak" 在添加换行符和不添加换行符之间切换。其他输入只是直接添加到 JTextPane 中。

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.text.*;

public class JTextPaneTest extends JFrame implements Runnable, ActionListener {

    JTextPane textPane;
    JTextField input;
    Style styleA;
    SimpleAttributeSet styleB;
    StyledDocument doc;
    boolean setStyleA = false;
    boolean linebreak = true;

    public JTextPaneTest() {
        SwingUtilities.invokeLater(this);
    }

    @Override
    public void run() {

        // Text Pane
        textPane = new JTextPane();
        doc = textPane.getStyledDocument();
        JScrollPane scrollPane = new JScrollPane(textPane);

        // Styles
        styleA = doc.addStyle("styleA", null);
        styleB = new SimpleAttributeSet();

        // Input
        input = new JTextField();
        input.addActionListener(this);

        // Add everything to the window
        this.getContentPane().add(scrollPane, BorderLayout.CENTER);
        getContentPane().add(input, BorderLayout.SOUTH);

        // Prepare and show window
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        pack();
        this.setSize(400, 300);
        setVisible(true);
    }

    public static void main(String[] args) {
        new JTextPaneTest();
    }

    void insert(final String text) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    if (setStyleA) {
                        // Changing styleA, which is added to the StyledDocument
                        // seems to make the problem worse
                        StyleConstants.setForeground(styleA, Color.blue);
                    }
                    else {
                        StyleConstants.setForeground(styleB, Color.blue);
                    }
                    // Not adding a linebreak seems to make the problem worse
                    String addLinebreak = "";
                    if (linebreak) {
                        addLinebreak = "\n";
                    }
                    doc.insertString(doc.getLength(), text+addLinebreak, null);
                } catch (BadLocationException ex) {
                    Logger.getLogger(JTextPaneTest.class.getName()).log(Level.SEVERE, null, ex);
                }

            }
        });
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        String text = input.getText();

        if (text.equals("test")) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // Insert some text to kind of simulate chat messages coming in
                    for (int i = 0; i < 500; i++) {
                        try {
                            Thread.sleep(250);
                        } catch (InterruptedException ex) {
                            Logger.getLogger(JTextPaneTest.class.getName()).log(Level.SEVERE, null, ex);
                        }
                        insert(i + " Test text to sort of simulate a chat message");
                    }
                }
            }).start();
        }
        // Insert text that seems to break something
        // Example 1:
        else if (text.equals("insert1")) {
            insert("\uD83D\uDE3A");
        }
        // Example 2:
        else if (text.equals("insert2")) {
            insert("\u0E07");
        }
        // Toggle changing styleA or styleB
        else if (text.equals("style")) {
            if (this.setStyleA) {
                setStyleA = false;
                insert("Style: B");
            }
            else {
                setStyleA = true;
                insert("Style: A");
            }
        }
        // Toggle printing a linebreak after each insert
        else if (text.equals("linebreak")) {
            if (this.linebreak) {
                linebreak = false;
                insert("Linebreak: OFF");
            }
            else {
                linebreak = true;
                insert("Linebreak: ON");
            }
        }
        // Output entered text
        else {
            insert(input.getText());
            input.setText("");
        }
    } 
}

现在的问题是,那里发生了什么。这是一个已知的错误吗?难道我做错了什么?添加单个字符会产生这种效果似乎很奇怪。即使渲染成本高一点,也不会造成那么大的麻烦。

如果是 Java 错误,我可以做些什么来解决?也许以某种方式过滤受影响的角色?但我什至不知道那些是什么。如果我做错了什么,那是什么?也许我必须在插入之前以某种方式准备文本?改变它的编码?也许这是我需要改变的非常基本和简单的东西?请帮忙。 :)

更新: 下图显示了插入 5000 行文本(大约需要 20 分钟)时发生的情况,左边没有做任何特殊处理,右边插入一个麻烦的字符后。完成后我在 JConsole 中请求了一个垃圾收集,左边的下降到大约 10 MB,而右边的只下降到大约 45 MB,考虑到唯一的区别是一个插入的字符,这明显更多。之后的下降只是 JConsole 断开连接。您还可以看到右侧的 CPU 使用率高出约 0.5 个百分点。我重复了这个测试几次,结果总是一样的。这没有使问题更加明显的换行符/样式。

【问题讨论】:

    标签: java swing unicode memory-leaks jtextpane


    【解决方案1】:

    这就是我所做的:

    1. 运行 SSCCE 程序
    2. 附加 JVisualVM 并开始内存分析器
    3. 让程序初始化并稳定堆;强制 GC 并从分析器中获取快照。
    4. 在程序中输入“test”,让它完成添加新内容
    5. 从 JVisualVM 强制 GC 并从分析器获取快照
    6. 在程序中输入“insert1”和“insert2”生成问题字符
    7. 在程序中输入“test”以生成额外的正常内容并让它完成
    8. 从 JVisualVM 强制 GC 并从分析器获取快照,同时让 JVisualVM 生成堆转储

    我看到你在问题中提到的内容,但想补充一下:

    • 特殊字符确实使用与普通示例文本不同的呈现路径。例如,比较快照 (3) 和 (5) 之间的差异,仅显示 sun.font.* 包中的一个类。快照 (5) 和 (8) 之间的差异表明现在使用了大约 40 个额外的类。其中包括您提到的类:sun.font.StandardGlyphVectorsun.font.ExtendedTextSourceLabelsun.font.StandardTextSourcesun.font.DelegatingShape

    • 在我的分析运行中,在上面提到的类中,大多数都有大约 850 个活动对象。但是sun.font.DelegatingShape 是一个异常值,有大约 20,000 多个活动对象。

    • 我使用 JVisualVM 来探索最终的堆转储并专注于 DelegatingShape 类。这些对象持有对不同java.awt.geom.Rectangle2D$Float 对象的引用。这两者都由StandardGlyphVector 中的Shape[] 数组保持活动状态,并与ExtendedTextSourceLabel 共享。每个数组包含约 49 个非空元素。

    • 查看源代码,这些数组由软引用保存,作为单个字形的视觉边界框的一种缓存(参见:StandardGlyphVector.getGlyphVisualBounds())。好消息是,只能通过软引用访问的对象可以被垃圾回收,并且它们本身不会直接构成内存泄漏。虚拟机将尽可能将它们留在内存中(增加堆)。如果这些物品是通过其他方式强烈持有的,那么它们将永远不会被收集;目前我没有注意到任何明显的强引用。

    但为什么有这么多 ExtendedTextSourceLabel?长话短说,您的 JTextPane 是在 javax.swing.text.BoxView 之上实现的,在通过您的文档插入 ~1002 行之后,它包含 ~4004 ParagraphView 子对象。每个视图都包含自己的TextLayoutStrategy,并且在遍历大量其他对象后,会保存这些ExtendedTextSourceLabel 实例。

    因此,在渲染时间和内存消耗方面,支持 Unicode 的某些子集可能会更加昂贵。我没有发现任何内存“泄漏”的迹象除了您的示例在 JTextPane 的样式文档中保留“聊天对话”的整个历史记录 .你能做什么?

    • 仅在 JTextPane 中显示有限部分的聊天历史记录,例如仅显示最近的 N 个条目。

    • 将聊天记录保存在 Swing 渲染图之外的其他数据结构中。您需要自己管理滚动到 JTextPane 中文本的“页入”和“页出”部分,因此它只需要渲染整个历史的一小部分。

    编辑:分析运行 #2

    "AWT-EventQueue-0" prio=10 tid=0x00007ff38028c000 nid=0x5f74 runnable [0x00007ff3745db000]
    java.lang.Thread.State: RUNNABLE
    at javax.swing.text.AbstractDocument$BranchElement.getElementIndex(AbstractDocument.java:2389)
        at javax.swing.text.CompositeView.getViewIndexAtPosition(CompositeView.java:579)
        at javax.swing.text.FlowView$LogicalView.getViewIndexAtPosition(FlowView.java:692)
        at javax.swing.text.CompositeView.getViewIndex(CompositeView.java:497)
        at javax.swing.text.TextLayoutStrategy$AttributedSegment.getAttribute(TextLayoutStrategy.java:520)
        at sun.text.bidi.BidiBase.setPara(BidiBase.java:2711)
        at java.text.Bidi.<init>(Bidi.java:134)
        at java.awt.font.TextMeasurer.initAll(TextMeasurer.java:208)
        at java.awt.font.TextMeasurer.<init>(TextMeasurer.java:167)
        at java.awt.font.LineBreakMeasurer.<init>(LineBreakMeasurer.java:310)
    

    随着“换行符关闭”,性能弹坑完全停止。我进行了多个线程转储,共同点是 LineBreakMeasurer;我选择了上面的跟踪,因为它表明它必须处理“bidi”(双向)字符。

    只要我不触摸样式或换行选项,这对我来说似乎不是问题。

    【讨论】:

    • 尽管我在实际应用程序中已经将聊天线的数量限制为最多 250 条(由于它们从顶部删除的方式,它大多低于 200 条),但它需要几秒钟才能完成插入有问题的文本行。我不知道是什么让它变得更糟,除了它有样式文本,但没有有问题的字符它工作得很好。即使渲染某些字符的成本更高,对于单个插入的字符,性能下降似乎也非常不成比例。感谢您的工作,但我现在仍然不知道如何继续.. :(
    • 特殊字符可能会禁用优化并使整个文档回退到替代实现或呈现策略。因此,虽然它是由单个字符引起的,但它显然会影响整个文档。我看到的对象数量似乎与文本行数以及所有行中唯一字形的数量非常相关。
    • 好吧,这很不幸。如果我在我的应用程序中没有发现任何其他会导致这方面的额外性能损失的东西,我要么必须实现我自己的文本组件(如果这甚至可以工作的话),要么过滤掉除我知道是安全的那些之外的所有字符。
    • 如果有帮助,当我运行我的分析器时,我确实使用你的换行或样式命令;我只查看了与添加特殊字符相关的默认模式的前后关系。我没有注意到我的环境有任何明显的缓慢。内存增加,是的,但对于每秒预期的少量消息来说,速度肯定是可以接受的。那么你真的需要换行/样式功能吗?
    • 我什至在我的申请中都没有这些,它们只是在 SSCCE 中,因为我发现它们更慢了它。但不幸的是,在我的应用程序中它仍然比在 SSCCE 中慢。如果不是很明显,我现在不会在这几个星期了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-12
    • 2012-10-25
    • 2017-10-20
    • 2014-03-09
    • 1970-01-01
    • 1970-01-01
    • 2017-05-09
    相关资源
    最近更新 更多