残りの操作をループで実行するJavaスレッドが他のすべてのスレッドをブロックする


123

次のコードスニペットは2つのスレッドを実行します。1つは毎秒の単純なタイマーログ、2つ目は剰余演算を実行する無限ループです。

public class TestBlockingThread {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);

    public static final void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int i = 0;
            while (true) {
                i++;
                if (i != 0) {
                    boolean b = 1 % i == 0;
                }
            }
        };

        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    public static class LogTimer implements Runnable {
        @Override
        public void run() {
            while (true) {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
            }
        }
    }
}

これにより、次の結果が得られます。

[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004

無限タスクが他のすべてのスレッドを13.3秒間ブロックする理由がわかりません。スレッドの優先順位やその他の設定を変更しようとしましたが、何も機能しませんでした。

これを修正するための提案(OSコンテキスト切り替え設定の微調整を含む)がある場合は、お知らせください。


8
@MarthinはGCではありません。JITです。実行する-XX:+PrintCompilationと、拡張遅延が終了したときに次のメッセージが表示
Andreas

4
それは私のシステムで再現しますが、唯一の変更点は、ログ呼び出しをSystem.out.printlnに置き換えたことです。Runnableのwhile(true)ループ内に1msのスリープを導入すると、他のスレッドの一時停止がなくなるため、スケジューラーの問題のようです。
JJF

3
私がそれをお勧めするわけではありませんが、を使用して JITを無効にしても-Djava.compiler=NONE、起こりません。
Andreas

3
おそらく、単一のメソッドに対してJITを無効にすることができます。特定のメソッド/クラスについては、Java JIT
Andreas、

3
このコードには整数除算はありません。タイトルと質問を修正してください。
ローンの侯爵

回答:


94

ここですべての説明をした後(Peter Lawreyのおかげです)、この一時停止の主な原因はループ内のセーフポイントに到達することがめったにないため、JITでコンパイルされたコード置換のためにすべてのスレッドを停止するのに長い時間がかかることです。

しかし、私はさらに深く調べてセーフポイントにめったに到達しない理由を見つけることにしました。whileこの場合、ループのバックジャンプが「安全」でない理由が少しわかりにくいことに気づきました。

だから私は-XX:+PrintAssembly助けるためにその栄光の中で召喚します

-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel

調査の結果、ラムダC2コンパイラの3回目の再コンパイル後に、セーフポイントポーリングがループ内で完全に破棄されたことがわかりました。

更新

プロファイリングの段階では、変数iが0に等しいことはありませんでしC2た。そのため、この分岐を投機的に最適化し、ループを次のように変換しました

for (int i = OSR_value; i != 0; i++) {
    if (1 % i == 0) {
        uncommon_trap();
    }
}
uncommon_trap();

もともと無限ループは、カウンターを持つ通常の有限ループに再形成されたことに注意してください!有限カウントループでセーフポイントポーリングを排除するためのJIT最適化により、このループにもセーフポイントポーリングはありませんでした。

しばらくしてにi戻り0、珍しい罠が仕掛けられました。メソッドは最適化されなくなり、インタプリタで実行が継続されました。新しい知識での再コンパイル中にC2、無限ループを認識してコンパイルをあきらめました。メソッドの残りの部分は、適切なセーフポイントを使用してインタプリタで続行されました。

Nitsan Wakartによる、セーフポイントとこの特定の問題を取り上げた、必読の優れたブログ投稿「セーフポイント:意味、副作用、およびオーバーヘッド」があります。

非常に長いカウントループでのセーフポイントの削除が問題であることがわかっています。バグJDK-5014723Vladimir Ivanovに感謝)がこの問題に対処しています。

回避策は、バグが最終的に修正されるまで利用できます。

  1. 使用してみてください-XX:+UseCountedLoopSafepoints(これにより、全体的なパフォーマンス低下し、JVMがクラッシュする可能性があり JDK-8161147ます)。それを使用した後、C2コンパイラはバックジャンプでセーフポイントを維持し続け、元の一時停止が完全に消えます。
  2. 次を使用して、問題のあるメソッドのコンパイルを明示的に無効にすることができます
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. または、手動でセーフポイントを追加してコードを書き直すこともできます。たとえばThread.yield()、サイクルの終わりにコールしたりint ilong i(ありがとう、Nitsan Wakartに)変更したりしてもポーズが修正されます。


7
これは、修正方法の質問に対する真の答えです。
Andreas

警告:JVMをクラッシュさ-XX:+UseCountedLoopSafepointsせる可能性があるため、本番環境では使用しないでください。これまでの最善の回避策は、長いループを手動で短いループに分割することです。
アパンギン2016

@apanginああ。とった!ありがとうございます:)それがc2セーフポイントを削除する理由です!しかし、私が得られなかったもう1つは、次の予定です。私が見る限り、ループのアンロール(?)後にセーフポイントが残っておらず、stwを実行する方法がないようです。ある種のタイムアウトが発生して最適化解除が行われるのですか?
vsminkov 2016

2
以前のコメントは正確ではありませんでした。これで何が起こるかは完全に明らかです。プロファイリング段階でiは決して0ではないので、ループは投機的にfor (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();通常の有限カウントループのようなものに変換されます。いったんiラップは0に戻り、珍しいトラップが、取られる方法は、最適化を解除され、インタプリタで進行します。新しい知識での再コンパイル中に、JITは無限ループを認識し、コンパイルを中止します。メソッドの残りの部分は、適切なセーフポイントを使用してインタープリターで実行されます。
アパンギン2016

1
intの代わりにiaを長くすると、ループが「カウントされなくなり」、問題が解決します。
Nitsan Wakart 2016

64

要するに、あなたが持っているループi == 0は、到達したときを除いて、その中に安全なポイントがありません。このメソッドがコンパイルされ、コードが置き換えられるようにトリガーする場合、すべてのスレッドを安全なポイントにする必要がありますが、これには非常に長い時間がかかり、コードを実行しているスレッドだけでなく、JVM内のすべてのスレッドもロックされます。

以下のコマンドラインオプションを追加しました。

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation

さらに時間がかかるように見える浮動小数点を使用するようにコードを変更しました。

boolean b = 1.0 / i == 0;

そして、私が出力で見るのは

timeElapsed=100
Application time: 0.9560686 seconds
  41423  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds
Application time: 0.0000219 seconds
Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds
  41424  281 %     3       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
timeElapsed=40473
  41425  282 %     4       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
  41426  281 %     3       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
timeElapsed=100

注:コードを置き換えるには、スレッドを安全な場所で停止する必要があります。ただし、このような安全なポイントに到達するのは非常にまれであるように見えます(おそらくi == 0、タスクを

Runnable task = () -> {
    for (int i = 1; i != 0 ; i++) {
        boolean b = 1.0 / i == 0;
    }
};

同様の遅延が発生します。

timeElapsed=100
Application time: 0.9587419 seconds
  39044  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (28 bytes)   made not entrant
Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds
Application time: 0.0000087 seconds
Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds
timeElapsed=38100
timeElapsed=100

ループにコードを注意深く追加すると、遅延が長くなります。

for (int i = 1; i != 0 ; i++) {
    boolean b = 1.0 / i / i == 0;
}

取得

 Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds

ただし、常に安全なポイントを持つネイティブメソッドを使用するようにコードを変更します(組み込みでない場合)。

for (int i = 1; i != 0 ; i++) {
    boolean b = Math.cos(1.0 / i) == 0;
}

プリント

Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds

注:if (Thread.currentThread().isInterrupted()) { ... }ループに追加すると、安全なポイントが追加されます。

注:これは16コアマシンで発生したため、CPUリソースが不足することはありません。


1
JVMのバグですよね?ここで「バグ」とは、仕様の違反ではなく、実装の重大な問題を意味します。
usr

1
@vsminkovは、セーフポイントがないために世界を数分間停止させることができるので、バグとして扱う必要があるようです。ランタイムは、長い待機を回避するためにセーフポイントを導入する責任があります。
Voo

1
@Vooしかし、一方で、すべてのバックジャンプでセーフポイントを維持すると、CPUサイクルが大量に消費され、アプリケーション全体のパフォーマンスが著しく低下する可能性があります。しかし、私はあなたに同意します。その特定のケースでは、セーフポイントを維持することは合法に見えます
vsminkov

9
@Vooまあ... パフォーマンスの最適化に関してはいつもこの絵を思い出します:D
vsminkov

1
.NETはここにセーフポイントを挿入します(ただし、.NETは生成コードが遅い)。考えられる解決策は、ループをチャンク化することです。2つのループに分割し、内部が1024要素のバッチをチェックしないようにし、外部ループがバッチとセーフポイントを駆動するようにします。オーバーヘッドを概念的には1024倍に削減しますが、実際には少なくなります。
usr

26

理由の答えが見つかりました。それらはセーフポイントと呼ばれ、GCが原因で発生するStop-The-Worldとして最もよく知られています。

この記事を参照してください。JVMでのStop-the-Worldポーズのロギング

さまざまなイベントにより、JVMがすべてのアプリケーションスレッドを一時停止する可能性があります。このような一時停止は、Stop-The-World(STW)一時停止と呼ばれます。STWの一時停止がトリガーされる最も一般的な原因は、ガベージコレクション(githubの例)ですが、さまざまなJITアクション(例)、バイアスロックの取り消し(例)、特定のJVMTI操作など、アプリケーションを停止する必要があります。

アプリケーションスレッドを安全に停止できるポイントは、サプライズ、セーフポイントと呼ばれます。この用語は、すべてのSTW一時停止を指す場合にもよく使用されます。

GCログが有効になっていることは多かれ少なかれ一般的です。ただし、これはすべてのセーフポイントに関する情報をキャプチャするわけではありません。すべてを取得するには、次のJVMオプションを使用します。

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

GCを明示的に参照している名前付けについて疑問に思っている場合は、心配しないでください。これらのオプションをオンにすると、ガベージコレクションの一時停止だけでなく、すべてのセーフポイントがログに記録されます。上記のフラグを指定して次の例(githubのソース)を実行すると、

HotSpot用語集を読んで、これを定義します。

セーフポイント

すべてのGCルートが既知であり、すべてのヒープオブジェクトの内容が一貫しているプログラム実行中のポイント。グローバルな観点からすると、GCを実行する前に、すべてのスレッドがセーフポイントでブロックする必要があります。(特別な場合として、JNIコードを実行するスレッドは、ハンドルのみを使用するため、実行を継続できます。セーフポイントの間、ハンドルの内容をロードする代わりにブロックする必要があります。)ローカルの観点から、セーフポイントは区別されるポイントです実行中のスレッドがGCをブロックする可能性があるコードのブロック内。ほとんどの通話サイトはセーフポイントとして認められています。すべてのセーフポイントで当てはまる強い不変条件があり、非セーフポイントでは無視される場合があります。コンパイルされたJavaコードとC / C ++コードの両方がセーフポイント間で最適化されますが、セーフポイント間では最適化されません。JITコンパイラは、各セーフポイントでGCマップを発行します。VMのC / C ++コードは、様式化されたマクロベースの規則(たとえば、TRAPS)を使用して、潜在的なセーフポイントをマークします。

上記のフラグで実行すると、次の出力が得られます。

Application time: 0.9668750 seconds
Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds
timeElapsed=1015
Application time: 1.0148568 seconds
Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds
timeElapsed=1015
timeElapsed=1014
Application time: 2.0453971 seconds
Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds
timeElapsed=11732
Application time: 1.0149263 seconds
Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds
timeElapsed=1015

3番目のSTWイベントに注意してください:
停止した合計時間: 10.7951187秒
スレッドの停止にかかった時間: 10.7950774秒

JIT自体はほとんど時間はかかりませんでしたが、JVMがJITコンパイルを実行することを決定すると、STWモードに入りましたが、コンパイルされるコード(無限ループ)には呼び出しサイトがないため、セーフポイントに到達することはありませんでした。

STWは、JITが最終的に待機をあきらめ、コードが無限ループにあると結論したときに終了します。


「セーフポイント-すべてのGCルートが既知であり、すべてのヒープオブジェクトの内容が一貫しているプログラム実行中のポイント」 -ローカル値タイプの変数のみを設定/読み取るループでは、これが当てはまらないのはなぜですか?
BlueRaja-Danny Pflughoeft 2016

BlueRaja-DannyPflughoeft I @で、この質問に答えることをしようとした私の答え
vsminkov

5

自分でコメントスレッドといくつかのテストを行った後、一時停止はJITコンパイラが原因であると思います。JITコンパイラーがこれほど長い時間を費やしている理由は、私のデバッグ能力を超えています。

しかし、これを防ぐ方法を尋ねただけなので、解決策があります:

無限ループをJITコンパイラーから除外できるメソッドに入れます

public class TestBlockingThread {
    private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName());

    public static final void main(String[] args) throws InterruptedException     {
        Runnable task = () -> {
            infLoop();
        };
        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    private static void infLoop()
    {
        int i = 0;
        while (true) {
            i++;
            if (i != 0) {
                boolean b = 1 % i == 0;
            }
        }
    }

次のVM引数を使用してプログラムを実行します。

-XX:CompileCommand = exclude、PACKAGE.TestBlockingThread :: infLoop(PACKAGEをパッケージ情報に置き換えます)

メソッドがいつJITコンパイルされるかを示す次のようなメッセージが表示されます。
###コンパイルを除く:staticblocking.TestBlockingThread :: infLoop
このクラスをブロッキングと呼ばれるパッケージに入れたことに気付くでしょ う。


1
コンパイラは長い間、問題が場合を除き、ループ内で何も存在しないため、コードが安全なポイントに到達していないであることを取っていませんi == 0
ピーターLawrey

@PeterLawreyしかし、whileループのサイクルの終わりがセーフポイントではないのはなぜですか?
vsminkov 2016

@vsminkovセーフポイントがあるようですが、if (i != 0) { ... } else { safepoint(); }これは非常にまれです。すなわち。ループを終了/中断した場合、ほぼ同じタイミングになります。
Peter Lawrey、

@PeterLawreyの調査の結果、ループのバックジャンプにセーフポイントを設定するのが一般的であることがわかりました。この特定のケースの違いは何なのか気になります。多分私はナイーブだが、私は戻ってジャンプが「安全」ではありませんない理由見ない
vsminkov

@vsminkov JITはセーフポイントがループ内にあると認識しているため、最後にセーフポイントを追加しないと思います。
Peter Lawrey、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.