8

私のチャット クライアントには、テキストが挿入される JTextPane があり、1 秒あたり最大数行まで可能です。通常は、長時間 (たとえば 1 時間) であっても正常に動作しますが、非常に遅くなり、多くの 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 使用率が数パーセント増加します) が、メモリの使用量が徐々に増えていきます。

改行を追加しないと、挿入されたすべてのテキストが 1 つのエンティティとして扱われると思いますが、StyledDocument に追加された Style を変更すると、ドキュメント全体が何らかの形で更新される可能性があります。すでに挿入されたテキスト。

これが SSCCE (jdk1.7.0_21 でテスト済み) で、単純なコマンド入力を使用しています。 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("");
        }
    } 
}

問題は今、そこで何が起こっているかです。既知のバグですか?私は何か間違ったことをしていますか?キャラクターを1つ追加するだけでその効果が得られるのは奇妙に思えます。レンダリングのコストが多少高くなったとしても、それほど問題にはならないはずです。

Java のバグである場合、回避策として何ができますか? 影響を受けたキャラクターを何らかの方法でフィルタリングすることはできますか?しかし、私はそれらがどれであるかさえ知りません。私が何か間違ったことをしている場合、それは何ですか? テキストを挿入する前に、何らかの方法でテキストを準備する必要があるのでしょうか。エンコーディングを変更しますか?多分それは私が変更する必要がある非常に基本的で単純なものですか?助けてください。:)

更新: 次の図は、5000 行のテキスト (約 20 分かかります) を挿入しているときに何が起こるかを示しています。終了後に JConsole でガベージ コレクションを要求したところ、左のコレクションは約 10 MB まで減少しましたが、右のコレクションは約 45 MB までしか減少しませんでした。唯一の違いが挿入された文字 1 つだけであることを考慮すると、これは大幅に大きくなっています。その後のドロップは、JConsole の切断だけです。また、右側の方が CPU 使用率が約 0.5% 高くなっていることもわかります。このテストを数回繰り返しましたが、結果は常に同じでした。これには、問題をさらに目立たせる改行/スタイルがありません。

メモリーリーク

4

1 に答える 1

2

これが私がしたことです:

  1. SSCCE プログラムを実行する
  2. JVisualVM をアタッチし、メモリ プロファイラを開始します
  3. プログラムがヒープを初期化して安定させます。GC を強制し、プロファイラーからスナップショットを取得します。
  4. プログラムに「test」と入力し、新しいコンテンツの追加を完了させます
  5. JVisualVM から GC を強制し、プロファイラーからスナップショットを取得します
  6. 「insert1」と「insert2」をプログラムに入力して、問題の文字を生成します。
  7. プログラムに「test」と入力して、追加の通常のコンテンツを生成し、終了させます
  8. JVisualVM から GC を強制し、プロファイラーからスナップショットを取得し、JVisualVM にヒープ ダンプを生成させる

あなたの質問であなたが言及したことはわかりましたが、追加したいと思います:

  • 特殊文字は、通常のサンプル テキストとは別のレンダリング パスを使用します。たとえば、スナップショット (3) と (5) の違いを比較すると、sun.font.*パッケージから 1 つのクラスのみが示されます。スナップショット (5) と (8) の違いは、追加の ~40 クラスが使用されていることを示しています。これらには、あなたが言及したクラスが含まれています: sun.font.StandardGlyphVectorsun.font.ExtendedTextSourceLabelsun.font.StandardTextSource、およびsun.font.DelegatingShape

  • 上記のクラスのほとんどは、私のプロファイリング実行で、それぞれ約 850 個のライブ オブジェクトを持っています。しかしsun.font.DelegatingShape、ライブ オブジェクトが 20,000 以上ある異常値です。

  • JVisualVM を使用して最終的なヒープ ダンプを調査し、DelegatingShape クラスに注目しました。これらのオブジェクトは、個別のjava.awt.geom.Rectangle2D$Floatオブジェクトへの参照を保持しています。Shape[]これらは両方とも、内部の配列によって維持され、StandardGlyphVectorと共有されExtendedTextSourceLabelます。各配列には、最大 49 個の非 null 要素が含まれていました。

  • ソース コードを見ると、これらの配列は、個々のグリフのビジュアル バウンディング ボックスの一種のキャッシュとして、ソフト参照によって保持されます (「 」を参照StandardGlyphVector.getGlyphVisualBounds())。幸いなことに、ソフト参照を介してのみ到達可能なオブジェクトはガベージ コレクションが可能であり、それ自体がメモリ リークを直接構成することはありません。VM は可能な限りそれらをメモリに残します (ヒープを増やします)。オブジェクトが他の手段によって強力に保持されている場合、それらは決して収集されません。現時点では、明らかな強力な参照はありません。

しかし、なぜこれほど多くの ExtendedTextSourceLabels があるのでしょうか? 長い話を短くするために、ドキュメントを介して〜1002行を挿入した後、〜4004の子オブジェクトが含まJTextPaneれる上に実装されます。各ビューには独自のビューが含まれており、多数の他のオブジェクトをトラバースした後、それらのインスタンスが保持されます。javax.swing.text.BoxViewParagraphViewTextLayoutStrategyExtendedTextSourceLabel

そのため、Unicode の一部のサブセットをサポートすると、レンダリング時間とメモリ消費の両方でコストがかかる可能性があります。あなたの例がJTextPane のスタイル付きドキュメントに「チャット会話」の全履歴を保持する場合を除いて、メモリ「リーク」の兆候は見つかりませんでした。あなたは何ができますか?

  • 最新のNエントリのみなど、チャット履歴の限られた部分のみを JTextPane に表示します。

  • 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" (双方向) 文字を処理する必要があることを示しているためです。

スタイルや改行オプションに触れない限り、これは私にとって問題ではないようです。

于 2013-05-18T19:12:33.730 に答える