2015-10-07 5 views
24

私は、Java 8を実行するときに作成することができる奇妙な問題を実行しました.JVM自体で何らかの種類のタイミングエラーが発生しているかのように問題が発生します。断続的ですが、少なくとも私のテスト環境では再現性があります。問題は、明示的に設定されている配列値が破棄され、特定の状況下で0.0で置き換えられることです。具体的には、以下のコードでarray[0]は、new Double(r.nextDouble());の後に0.0と評価されています。次に、すぐにarray[0]の内容を見ると、値が1.0の正しい値になります。このテストケースを実行しているからの出力例は次のとおりです。Java 8の奇妙なタイミング/メモリの問題

claims array[0] != 1.0....array[0] = 1.0 
claims array[0] now == 1.0...array[0] = 1.0` 

私はWindows 7の64ビット版を実行しているがとのJDK 1.8_45で、Eclipse内およびコマンドラインからコンパイルするときの両方から、この問題を再現することができています、 1.8_51、および1.8_60。 1.7_51で問題を発生させることができません。別の64ビットWindows 7ボックスでも同じ結果が示されています。

この問題は、大きなソフトウェアでは見られませんでしたが、私は数行のコードに凝縮させました。以下はその問題を示す小さなテストケースです。それはかなり奇妙に見えるテストケースですが、エラーを引き起こすためにはすべてが必要であるようです。 Randomの使用は必須ではありません。r.nextDouble()をすべて2重の値に置き換えて問題を示すことができます。面白いことに、someArray[0] = .45;someArray[0] = r.nextDouble();に置き換えられた場合、問題は再現できませんでした(ただし、.45については特別なことはありません)。 Eclipseのデバッグも役に立たないので、タイミングがそれ以上変わることはありません。 System.err.println()ステートメントを配置しても、問題は表示されなくなります。

また、問題は断続的です。問題を再現するには、このテストケースを何回か実行する必要があります。私はそれを実行しなければならなかったほとんどが上記の出力を得る前に約10倍だと思います。 Eclipseでは、実行してから2〜2回してから、それが起こらなければそれを殺す。コマンドラインから同様に実行してください。実行されない場合は、CTRL+Cを終了してもう一度お試しください。それが起こるなら、それはかなり素早く起こると思われる。

私は過去にこのような問題を抱えていましたが、すべてスレッドの問題でした。私はここで何が起こっているのか理解できません - 私はバイトコード(これは1.7_51と1.8_45の間で全く同じです)を見ました。

ここには何が起こっているかに関するアイデアは何ですか?

import java.util.Random; 

public class Test { 
    Test(){ 
     double array[] = new double[1];  
     Random r = new Random(); 

     while(true){ 
      double someArray[] = new double[1];   
      double someArray2 [] = new double [2]; 

      for(int i = 0; i < someArray2.length; i++) { 
       someArray2[i] = r.nextDouble(); 
      } 

      // for whatever reason, using r.nextDouble() here doesn't seem 
      // to show the problem, but the # you use doesn't seem to matter either... 

      someArray[0] = .45; 

      array[0] = 1.0; 

      // commented out lines also demonstrate problem 
      new Double(r.nextDouble()); 
      // new Float(r.nextDouble(); 
      // double d = new Double(.1) * new Double(.3); 
      // double d = new Double(.1)/new Double(.3); 
      // double d = new Double(.1) + new Double(.3); 
      // double d = new Double(.1) - new Double(.3); 

      if(array[0] != 1.0){ 
       System.err.println("claims array[0] != 1.0....array[0] = " + array[0]); 

       if(array[0] != 1.0){ 
        System.err.println("claims array[0] still != 1.0...array[0] = " + array[0]); 
       }else { 
        System.err.println("claims array[0] now == 1.0...array[0] = " + array[0]); 
       } 

       System.exit(0); 
      }else if(r.nextBoolean()){ 
       array = new double[1]; 
      } 
     } 
    } 

    public static void main(String[] args) { 
     new Test(); 
    } 
} 
+0

悪いメモリをチェックしましたか? – wero

+3

これは再現できません。ここに期待どおりに動作します。 – marstran

+2

'double'は本質的に正確ではありません。これはあなたの問題ではないと確信していますか?それは複数のマシンには、すべての(私は3種類の64ビットWin7の箱の上に再現することができます)悪い記憶を持っているために起こる可能性がありません@wero –

答えて

21

更新は:私のオリジナルの答えは間違っていたとOnStackReplacementはちょうどこの特定のケースで問題を明らかにしたが、元のバグはエスケープ解析コードにあったようです。エスケープ解析は、オブジェクトが与えられたメソッドから逃げるかどうかを決定するコンパイラサブシステムです。エスケープされていないオブジェクトは、(ヒープ割り当ての代わりに)スカラー化することも、完全に最適化することもできます。私たちのテストエスケープでは、いくつかの作成されたオブジェクトが確実にメソッドをエスケープしないので、分析は重要です。

JDK 9 early access build 83をダウンロードしてインストールしましたが、そのバグが表示されなくなったことに気付きました。しかし、JDK 9の早期アクセスビルド82ではまだ存在しています。 b82とb83の間のchangelogには、関連するバグ修正が1つしか表示されません(私が間違っていれば私を修正してください):JDK-8134031 "インライン化とエスケープ解析を伴う複雑なコードのJITコンパイルが正しくありません"。コミットされたtestcaseはいくぶん似ています:大きなループ、ボックス内の値の突然の変化につながるいくつかのボックス(テストでは1要素の配列に似ています)は結果が黙って間違っています間違った値)。私たちの場合のように、問題は8u40より前には現れないと報告されています。 introduced fixは非常に短く、エスケープ解析ソースでは1行だけ変更されています。

OpenJDKのバグトラッカーによると、修正プログラムが既にbackported月に発売されるscheduledあるJDKの8u72のブランチにある、2016年には、今後の8u66にこの修正プログラムをバックポートするには遅すぎたようです。

エスケープ解析(-XX:-DoEscapeAnalysis)を無効にするか、割り当ての最適化を無効にする(-XX:-EliminateAllocations)ことをお勧めします。従って私より答えに@apangin was actually closer。以下は

私はJDK 8u25の問題を再現することはできません、


まずオリジナルの答えですが、JDKの8u40と8u60上で次のことができます。時にはそれが正しく、時にはそれが出力し、(無限ループで立ち往生)実行され、終了します。したがって、JDKの8u25へのダウングレードが受け入れられる場合は、これを検討することができます。 javacで後で修正が必要な場合(特にlambdaを含む多くのものが1.8u40で修正されました)、新しいjavacでコンパイルできますが、古いJVMで実行できます。

この特定の問題は、OnStackReplacementメカニズム(OSRが第4層で発生した場合)のバグである可能性があります。 OSRに精通していない場合は、this answerをお読みください。 OSRは確かにあなたのケースでは、少し奇妙な方法で発生します。ここで失敗した実行の-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+TraceNMethodInstalls%はOSR JITを意味し、@ 28(3)(4)は階層レベルを意味し、OSRバイトコードの位置を意味する)は次のとおり

... 
    91 37 %  3  Test::<init> @ 28 (194 bytes) 
Installing osr method (3) Test.<init>()V @ 28 
    93 38  3  Test::<init> (194 bytes) 
Installing method (3) Test.<init>()V 
    94 39 %  4  Test::<init> @ 16 (194 bytes) 
Installing osr method (4) Test.<init>()V @ 16 
    102 40 %  4  Test::<init> @ 28 (194 bytes) 
    103 39 %  4  Test::<init> @ -2 (194 bytes) made not entrant 
... 
Installing osr method (4) Test.<init>()V @ 28 
    113 37 %  3  Test::<init> @ -2 (194 bytes) made not entrant 
claims array[0] != 1.0....array[0] = 1.0 
claims array[0] now == 1.0...array[0] = 1.0 

したがってTIER4でOSRは、二つの異なるバイトコード・オフセットを発生しますである(16オフセットwhileループエントリポイント)およびオフセット28(これはネストされたforループエントリポイントです)。 OSRでコンパイルされた両方のバージョンのメソッドのコンテキスト転送中に競合状態が発生し、コンテキストが壊れているようです。実行がOSRメソッドに引き渡されるときには、arrayrのようなローカル変数の値を含む現在のコンテキストをOSR'edメソッドに転送する必要があります。ここで何か悪いことが起こります:おそらく短時間です<init>@16 OSRバージョンが動作し、<init>@28に置き換えられますが、少し遅れてコンテキストが更新されます。 OSRのコンテキスト転送は、「割り振りを排除する」最適化を妨げる可能性があります(この最適化をオフに切り替えることでわかるように@apanginがあなたのケースで役立ちます)。私の専門知識は、ここでさらに掘り下げるのに十分ではありません。恐らく@apanginはコメントするかもしれません。これとは対照的に

は、通常の実行中にティア4 OSR方式のコピーが1つだけ作成され、インストールされています:

... 
Installing method (3) Test.<init>()V 
    88 43 %  4  Test::<init> @ 28 (194 bytes) 
Installing osr method (4) Test.<init>()V @ 28 
    100 40 %  3  Test::<init> @ -2 (194 bytes) made not entrant 
    4592 44  3  java.lang.StringBuilder::append (8 bytes) 
... 

したがって、この場合、2つのOSRのバージョンの間には競争が発生していないと、すべてが完璧に動作しているようです。

あなたが別の方法に外側のループ本体を移動した場合、問題がまた消える:このバグにそれを打つために

int i=0; 
someArray2[i++] = r.nextDouble(); 
someArray2[i++] = r.nextDouble(); 

:ネストされたforループがバグを取り除くアンロール

import java.util.Random; 

public class Test2 { 
    private static void doTest(double[] array, Random r) { 
     double someArray[] = new double[1]; 
     double someArray2[] = new double[2]; 

     for (int i = 0; i < someArray2.length; i++) { 
      someArray2[i] = r.nextDouble(); 
     } 

     ... // rest of your code 
    } 

    Test2() { 
     double array[] = new double[1]; 
     Random r = new Random(); 

     while (true) { 
      doTest(array, r); 
     } 
    } 

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

またマニュアル同じ方法で少なくとも2つのネストされたループを持つべきだと思われるので、OSRは異なるバイトコード位置で発生する可能性があります。したがって、特定のコードで回避するには、ループ本体を別のメソッドに抽出するだけで同じことができます。

代替ソリューションを-XX:-UseOnStackReplacementで完全にOSRを無効にすることです。プロダクションコードではほとんど役に立ちません。ループカウンターは引き続き動作し、many-iterations-loopのメソッドが少なくとも2回呼び出された場合、2回目の実行はJITコンパイルされます。また、長いループを持つメソッドがOSRが無効になっているためJITコンパイルされていなくても、それが呼び出すメソッドは引き続きJITコンパイルされます。私はズールーでコードを実行した後

+0

素晴らしい仕事。これをバグレポートに含めてください.JDK開発者が問題を解決するのに役立つかもしれません。私ができる場合、私は:-) – Axel

+0

うん、オンスタック交換は、あなたがそのコードのパフォーマンス関連のある大きな長時間実行されているメソッドを持っている場合は、一般的なアプリケーションコードが一致しないパターンである、ことができますが、典型的な... +2を与えるだろう人工的なベンチマークコード。 – Holger

+0

うわー、素晴らしい仕事!私はすでにバグレポートを提出していましたが、まだレビュー中です。それが受け入れられたら、私はそれに情報を加えることができると仮定して、私は確かにこれを含めるでしょう。再度、感謝します! – bcothren

0

私は、Oracle VMで http://www.javaspecialists.eu/archive/Issue234.html

に掲載のコードでズールー(OpenJDKのの認定ビルド)でこのエラーを再現することができ、私はこのエラーを再現することができます。 Zuluが共有ルックアップキャッシュを汚染しているようです。この場合の解決策は、-XX:-EnableSharedLookupCacheを使用してコードを実行することです。

+1

Azulには、2つのJVM ZuluとZingがあります。あなたが提供するリンク(壊れている)から、ZingではなくZuluを参照しているようです。 Zuluは完全にOpenJDKのコードをテストしてサポートしているOpenJDKビルドです。これは、同等のバージョンで同じ動作を示すはずです。 Zingは全く違う獣です。 –