32

問題は次のとおりです。

メモリ使用量が制御不能になる

ご覧のとおり、メモリ使用量が制御不能に膨れ上がります! 何が起こっているのかを把握している間にメモリ不足エラーを回避するためだけに、JVM に引数を追加してヒープサイズを増やす必要がありました。良くない!

基本的なアプリケーションの概要 (コンテキスト用)

このアプリケーションは、(最終的には)自動化の目的で、基本的な画面上の CV とテンプレート マッチング タイプのものに使用される予定です。画面を見るためにできるだけ高いフレームレートを達成し、一連の個別の消費者スレッドを介してすべての処理を処理したいと考えています。

標準の Robot クラスはスピードに関しては本当にひどいことがすぐにわかったので、ソースを開き、重複した労力と無駄なオーバーヘッドをすべて取り除き、FastRobot という名前の独自のクラスとして再構築しました。

クラスのコード:

public class FastRobot {
    private Rectangle screenRect;
    private GraphicsDevice screen;
    private final Toolkit toolkit;
    private final Robot elRoboto;
    private final RobotPeer peer;
    private final Point gdloc;
    private final DirectColorModel screenCapCM;
    private final int[] bandmasks;

    public FastRobot() throws HeadlessException, AWTException {
        this.screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
        this.screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
        toolkit = Toolkit.getDefaultToolkit();
        elRoboto = new Robot();
        peer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);

        gdloc = screen.getDefaultConfiguration().getBounds().getLocation();
        this.screenRect.translate(gdloc.x, gdloc.y);

        screenCapCM = new DirectColorModel(24,
                /* red mask */    0x00FF0000,
                /* green mask */  0x0000FF00,
                /* blue mask */   0x000000FF);
        bandmasks = new int[3];
        bandmasks[0] = screenCapCM.getRedMask();
        bandmasks[1] = screenCapCM.getGreenMask();
        bandmasks[2] = screenCapCM.getBlueMask();

        Toolkit.getDefaultToolkit().sync();
    }

    public void autoResetGraphicsEnv() {
        this.screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
        this.screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
    }

    public void manuallySetGraphicsEnv(Rectangle screenRect, GraphicsDevice screen) {
        this.screenRect = screenRect;
        this.screen = screen;
    }


    public BufferedImage createBufferedScreenCapture(int pixels[]) throws HeadlessException, AWTException {
//      BufferedImage image;
        DataBufferInt buffer;
        WritableRaster raster;

        pixels = peer.getRGBPixels(screenRect);
        buffer = new DataBufferInt(pixels, pixels.length);

        raster = Raster.createPackedRaster(buffer, screenRect.width, screenRect.height, screenRect.width, bandmasks, null);
        return new BufferedImage(screenCapCM, raster, false, null);
    }

    public int[] createArrayScreenCapture() throws HeadlessException, AWTException {
            return peer.getRGBPixels(screenRect);
    }

    public WritableRaster createRasterScreenCapture(int pixels[]) throws HeadlessException, AWTException {
    //  BufferedImage image;
        DataBufferInt buffer;
        WritableRaster raster;

        pixels = peer.getRGBPixels(screenRect);
        buffer = new DataBufferInt(pixels, pixels.length);

        raster = Raster.createPackedRaster(buffer, screenRect.width, screenRect.height, screenRect.width, bandmasks, null);
    //  SunWritableRaster.makeTrackable(buffer);
        return raster;
    }
}

本質的に、私がオリジナルから変更したのは、多くの割り当てを関数本体から移動し、それらをクラスの属性として設定して、毎回呼び出されないようにしたことだけです。これを行うと、実際にフレームレートに大きな影響がありました。電力が著しく不足しているラップトップでも、標準の Robot クラスでは最大 4 fps でしたが、FastRobot クラスでは最大 30 fps になりました。

最初のテスト:

メイン プログラムでメモリ不足エラーが発生したとき、FastRobot を監視するためにこの非常に単純なテストをセットアップしました。注: これは、上記のヒープ プロファイルを生成したコードです。

public class TestFBot {
    public static void main(String[] args) {
        try {
            FastRobot fbot = new FastRobot();

            double startTime = System.currentTimeMillis();
            for (int i=0; i < 1000; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

        } catch (AWTException e) {
            e.printStackTrace();
        }
    }
}

調べた:

毎回これを行うわけではありませんが、これは本当に奇妙です (そしてイライラします!)。実際、上記のコードでそれを行うことはめったにありません。ただし、複数の for ループが連続している場合 、メモリの問題は簡単に再現できます。

テスト 2

public class TestFBot {
    public static void main(String[] args) {
        try {
            FastRobot fbot = new FastRobot();

            double startTime = System.currentTimeMillis();
            for (int i=0; i < 1000; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 500; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 200; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

            startTime = System.currentTimeMillis();
            for (int i=0; i < 1500; i++)
                fbot.createArrayScreenCapture();
            System.out.println("Time taken: " + (System.currentTimeMillis() - startTime)/1000.);

        } catch (AWTException e) {
            e.printStackTrace();
        }
    }
}

調べた

制御不能なヒープは現在、約 80% の確率で再現可能です。私はプロファイラーをすべて調べましたが、最も注目すべき (私が思うに) ことは、ガベージ コレクターが 4 番目の最後のループが始まるとすぐに停止するように見えることです。

上記のコードからの出力は、次の時間を示しました。

Time taken: 24.282    //Loop1
Time taken: 11.294    //Loop2
Time taken: 7.1       //Loop3
Time taken: 70.739    //Loop4

ここで、最初の 3 つのループを合計すると 42.676 になります。これは、ガベージ コレクターが停止し、メモリが急増した正確な時間に疑わしいことに対応しています。

ここに画像の説明を入力

さて、これはプロファイリングを使った初めてのロデオです。言うまでもなく、ガベージ コレクションについて考えたのも初めてです。それは常にバックグラウンドで魔法のように機能するものでした。どちらかといえば、私は見つけました。

追加のプロフィール情報

ここに画像の説明を入力

Augusto は、メモリ プロファイルを調べることを提案しました。int[]「到達不能だが、まだ収集されていない」とリストされているものは 1500 以上あります。これらは確かに が作成するint[]配列ですpeer.getRGBPixels()が、何らかの理由で破棄されていません。残念ながら、 GC がそれらを収集しない 理由がわからないため、この追加情報は私の混乱を招くだけです。


小さなヒープ引数 -Xmx256m を使用したプロファイル:

評判の悪い Hot Licks の提案で、私は最大ヒープ サイズをかなり小さい値に設定しました。これにより、メモリ使用量1GBジャンプするのを防ぎますが、4回目の反復に入ったときにプログラムが最大ヒープサイズに膨らむ理由はまだ説明されていません。

ここに画像の説明を入力

ご覧のとおり、正確な問題はまだ存在しており、サイズが小さくなっています。;) この解決策の問題点は、何らかの理由で、プログラムがまだすべてのメモリを食い尽くしていることです。また、最初の反復から fps パフォーマンスに顕著な変化があり、メモリをほとんど消費しません。可能な限り多くのメモリを消費する最後の反復。

なぜそれが膨らんでいるの かという疑問が残ります。


「Force Garbage Collection」ボタンを押した後の結果:

jtahlborn の提案で、Force Garbage Collectionボタンを押しました。それは美しく働きました。1 GB のメモリ使用量から 60 MB 程度のベースラインまで下がります。

ここに画像の説明を入力

ということで、これで治りそうです。問題は、プログラムで GC にこれを強制するにはどうすればよいかということです。


関数のスコープにローカル ピアを追加した後の結果:

David Waters の提案でcreateArrayCapture()、ローカルPeerオブジェクトを保持するように関数を変更しました。

残念ながら、メモリ使用パターンに変化はありません。

ここに画像の説明を入力

3 回目または 4 回目の繰り返しではまだ巨大になります。


メモリ プールの分析:

さまざまなメモリ プールのスクリーンショット

すべてのプール:

すべてのプール

エデンプール:

エデンプール

古い世代:

旧世代
ほぼすべてのメモリ使用量がこのプールに収まるようです。

注: PS Survivor Space の使用量は (どうやら) 0 でした


私はいくつかの質問が残っています:

(a) Garbage Profiler グラフは、私が考えていることを意味していますか? それとも、相関関係と因果関係を混同していますか? 私が言ったように、私はこれらの問題で未知の領域にいます。

(b)ガベージ コレクタの場合...どうすればよいでしょうか.. ? 完全に停止し、プログラムの残りの部分で速度を落として実行するのはなぜですか?

(c) これを修正するにはどうすればよいですか?

何が起きてる?

4

2 に答える 2

3

オブジェクトの作成をメソッドからクラスのフィールドに移動したとおっしゃいました。移動した依存関係の 1 つは「ピア」でしたか? 例えば

peer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);

ピアは、オブジェクトの存続期間中に撮影されたすべてのスクリーンショットを保持している可能性があります。これは、ピアが範囲外に移動したとき、Robot のメソッドの終わり、FastRobot のクラスの存続期間中にクリアされます。

ピアの作成とスコープをメソッドに戻してみて、違いを確認してください。

public int[] createArrayScreenCapture() throws HeadlessException, AWTException {
        RobotPeer localPeer = ((ComponentFactory)toolkit).createRobot(elRoboto, screen);
        return localPeer.getRGBPixels(screenRect);
}

試行 2

ということで、これで治りそうです。問題は、GC にこれを強制的に実行させるにはどうすればよいかということです。

System.gc() を呼び出して、ガベージ コレクションを要求できます。これは要求ではなく要求であることに注意してください。JVM は、価値があると判断した場合にのみガベージ コレクションを実行します。

ご覧のとおり、正確な問題はまだ存在しており、サイズが小さくなっています。;) この解決策の問題点は、何らかの理由で、プログラムがまだすべてのメモリを食い尽くしていることです。また、最初の反復から fps パフォーマンスに顕著な変化があり、メモリをほとんど消費しません。可能な限り多くのメモリを消費する最後の反復。

なぜそれが膨らんでいるのかという疑問が残ります。

JVM は、絶対に必要な場合 (ほとんどのヒープが使用されている場合) にのみメジャー ガベージ コレクションを実行しようとします。(Young Generation 対 Old Generation と、Young Generation、Eden 空間、および Survivor 空間内に読み込まれます)。そのため、寿命の長い Java アプリケーションやメモリを大量に消費する Java アプリケーションは、最大ヒープ サイズ近くに収まることが予想されます。メモリが旧世代に移動するには、3 回のマイナー GC 実行に耐えなければならないことに注意してください (Eden => Survivor 1 => Survivor 2 => Old Generation [実行している JVM と選択した GC スキームによって異なります)。コマンドライン引数で。])

この動作が変化する理由については、さまざまな理由が考えられます。この最後のループは最長です。System.getCurrentTimeMillis() は、GC が別のスレッドに進むのに十分な時間ブロックしますか? では、問題は長いループでのみ発生しますか? スクリーンショットを撮るプロセスは私にはかなり低レベルに聞こえます.オペレーティングシステムカーネルへの呼び出しで実装されていると思います.これはカーネル空間でプロセスをブロックし、他のスレッドの実行を妨げますか? (これにより、バックグラウンド スレッドで実行されている gc が停止します)。

ガベージ コレクションの世界の紹介については、http://www.javacodegeeks.com/2012/01/practical-garbage-collection-part-1.htmlをご覧ください。または、Java メモリの説明 (SUN JVM)でさらに多くのリンクをヒープします。

それが助けになったことを願っています。

于 2013-02-28T17:46:46.133 に答える