jjzjj

java - 在 JTextPane 中插入一些字符会导致性能问题和内存泄漏

coder 2024-03-21 原文

我的聊天客户端有一个 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 的样式和另一个“换行符”在添加换行符和不添加换行符之间切换。其他输入只是直接添加到 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 个百分点。我重复了几次这个测试,结果总是一样的。这没有使问题更加明显的换行/样式内容。

    最佳答案

    这是我所做的:

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

  • 我看到您在问题中提到的内容,但想补充一点:
  • 特殊字符确实使用与普通示例文本不同的渲染路径。例如,比较快照 (3) 和 (5) 之间的差异,仅显示 sun.font.* 中的一个类。包裹。快照 (5) 和 (8) 之间的差异表明现在使用了额外的约 40 个类。这些包括您提到的类:sun.font.StandardGlyphVector , sun.font.ExtendedTextSourceLabel , sun.font.StandardTextSource , 和 sun.font.DelegatingShape .
  • 在上面提到的类中,在我的分析运行中,每个类都有大约 850 个 Activity 对象。但是sun.font.DelegatingShape是一个具有约 20,000 多个 Activity 对象的异常值。
  • 我使用 JVisualVM 来探索最终的堆转储并专注于 DelegatingShape 类。这些对象持有对不同 java.awt.geom.Rectangle2D$Float 的引用对象。这两个都通过 Shape[] 保持 Activity 状态。数组内部 StandardGlyphVector并与 ExtendedTextSourceLabel 共享.每个数组包含约 49 个非空元素。
  • 查看源代码,这些数组由软引用保存,作为单个字形的视觉边界框的一种缓存(参见:StandardGlyphVector.getGlyphVisualBounds())。好消息是只能通过软引用访问的对象可以被垃圾收集,并且不会直接构成内存泄漏。 VM 会尽可能长时间地将它们留在内存中(增加堆)。如果对象通过其他方式被 STRONGLY 持有,那么它们将永远不会被收集;我目前没有注意到任何明显的强引用。

  • 但是为什么有这么多 ExtendedTextSourceLabels 呢?长话短说,您的 JTextPanejavax.swing.text.BoxView 之上实现通过您的文档插入 ~1002 行后,其中包含 ~4004 ParagraphView子对象。每个 View 都包含自己的 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)
    

    随着“linebreaks OFF”性能下降到死胡同。我进行了多个线程转储,共同点是 LineBreakMeasurer;我选择了上面的跟踪,因为它表明它必须处理“bidi”(双向)字符。

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

    关于java - 在 JTextPane 中插入一些字符会导致性能问题和内存泄漏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16577977/

    有关java - 在 JTextPane 中插入一些字符会导致性能问题和内存泄漏的更多相关文章

    1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    2. Ruby 解析字符串 - 2

      我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?

    3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

      我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

    4. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

      作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

    5. ruby-on-rails - unicode 字符串的长度 - 2

      在我的Rails(2.3,Ruby1.8.7)应用程序中,我需要将字符串截断到一定长度。该字符串是unicode,在控制台中运行测试时,例如'א'.length,我意识到返回了双倍长度。我想要一个与编码无关的长度,以便对unicode字符串或latin1编码字符串进行相同的截断。我已经了解了Ruby的大部分unicode资料,但仍然有些一头雾水。应该如何解决这个问题? 最佳答案 Rails有一个返回多字节字符的mb_chars方法。试试unicode_string.mb_chars.slice(0,50)

    6. ruby - 将差异补丁应用于字符串/文件 - 2

      对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

    7. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

      大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

    8. ruby - 如何以所有可能的方式将字符串拆分为长度最多为 3 的连续子字符串? - 2

      我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123

    9. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    10. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

      我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

    随机推荐