スタックトレースとヒップダンプについて知りたいことすべて。 パート1

実践により、レポートの筋金入りの転写産物がうまくいくことが示されたため、続行することにしました。 今日、私たちのメニューには、JUGの1つに関するOdnoklassnikiのapanginとして知られるAndrey Panginによるレポートに基づいて作成された、エラーとキャッシュの検索と分析へのアプローチの混合があります。 7分間の2時間のレポートで、Andreiはスタックトレースとヒップダンプについて詳しく説明します。



投稿は非常に大きかったため、2つの部分に分割しました。 今、あなたは最初の部分を読んでいます、2番目の部分はここにあります







今日は、スタックトレースとヒップダンプについて話します-一方で、誰もが知っているトピック-常に新しいものを開くことができるようにします(このトピックの準備中にJVMのバグを見つけました)。



私たちのオフィスでこのレポートのトレーニングを行っていたとき、同僚の一人が尋ねました:「これはすべて非常に興味深いのですが、実際に誰にとっても有用ですか?」この会話の後、プレゼンテーションの最初のスライドにトピックに関する質問のあるページを追加しましたStackoverflow これは関連性があります。







私自身はOdnoklassnikiの主要なプログラマーとして働いています。 そして、たまたまJavaの内部で作業しなければならないことがよくありました。それを調整し、バグを探し、システムクラスを通して何かをヤンクします(完全に正当な方法ではない場合があります)。 そこから、今日あなたに提示したいほとんどの情報を得ました。 もちろん、これまでの経験はこれに大いに役立ちました。SunMicrosystemsで6年間働いていたとき、仮想Javaマシンの開発に直接関与していました。 ですから、JVM内と開発ユーザーの両方からこのトピックを知っています。



スタックトレース



スタックトレースの例外







初心者の開発者が「Hello world!」と書くと、実行がポップアップし、このエラーが発生したスタックトレースが表示されます。 そのため、大半の人はスタックトレースについていくつかのアイデアを持っています。



例に直行しましょう。



次の実験を1億回実行する小さなプログラムを作成しました。10個のlong型のランダム要素の配列を作成し、ソートされているかどうかを確認します。



package demo1; import java.util.concurrent.ThreadLocalRandom; public class ProbabilityExperiment {    private static boolean isSorted(long[] array) {        for (int i = 0; i < array.length; i++) {            if (array[i] > array[i + 1]) {                return false;            }        }        return true;    }    public void run(int experiments, int length) {        int sorted = 0;        for (int i = 0; i < experiments; i++) {            try {                long[] array = ThreadLocalRandom.current().longs(length).toArray();                if (isSorted(array)) {                    sorted++;                }            } catch (Exception e) {                e.printStackTrace();            }        }        System.out.printf("%d of %d arrays are sorted\n", sorted, experiments);    }    public static void main(String[] args) {        new ProbabilityExperiment().run(100_000_000, 10);    } }
      
      





実際、彼はソートされた配列を取得する確率を考慮します。これは1/n!



ほぼ等しくなり1/n!



。 よくあることですが、プログラマーは1つ間違いを犯しました。



 for (int i = 0; i < array.length; i++)
      
      





どうなるの? 実行、配列から出て。

何が問題なのか把握しましょう。 コンソールの表示:



 java.lang.ArrayIndexOutOfBoundsException
      
      





しかし、スタックトレースはありません。 どこへ行く?



HotSpot JVMにはこのような最適化があります。JVM自体がホットコードからスローする実行であり、この場合コードはホットです-1億回ひっくり返り、スタックトレースは生成されません。

これは、特別なキーを使用して修正できます。



 -XX:-OmitStackTraceInFastThrow
      
      





ここで例を実行してみてください。 すべて同じで、すべてのスタックトレースだけが所定の場所にあります。



このような最適化は、JVMがスローするすべての暗黙的な実行に対して機能します。配列の境界を越える、nullポインターの逆参照など。







彼らは最適化を考え出したので、何らかの理由でそれが必要ですか? スタックトレースがある場合、プログラマにとってより便利であることは明らかです。



実行の作成にかかるコストを測定しましょう(Dateなどの単純なJavaオブジェクトと比較してみましょう)。



 @Benchmark public Object date() { return new Date(); } @Benchmark public Object exception() { return new Exception(); }
      
      





JMHを使用して、単純なベンチマークを作成し、両方の操作にかかるナノ秒数を測定します。







実行の作成は、通常のオブジェクトの150倍の費用がかかります。

そして、ここではそれほど単純ではありません。 仮想マシンの場合、実行は他のオブジェクトと変わりませんが、解決策は、ほぼすべての建設設計者が何らかの方法で、この実行のスタックトレースを埋めるfillInStackTraceメソッドを呼び出すことになります。 時間がかかるスタックトレースを埋めています。







このメソッドはネイティブであり、VMランタイムに分類され、そこでスタックに沿って歩き、すべてのフレームを収集します。



fillInStackTraceメソッドはpublicであり、finalではありません。 再定義してみましょう。



 @Benchmark public Object exceptionNoStackTrace() { return new Exception() { @Override public Throwable fillInStackTrace() { return this; } }; }
      
      





スタックトレースなしで通常のオブジェクトを作成して実行すると、同じ時間がかかります。







スタックトレースなしで出口を作成する別の方法があります。 Java 7以降、ThrowableとExceptionには、追加パラメーターwritableStackTraceを持つコンストラクターが保護されています。



 protected Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace);
      
      





そこでfalseが渡されると、スタックトレースは生成されず、実行の作成は非常に高速になります。



スタックトレースなしで実行する必要があるのはなぜですか? たとえば、ループからすばやく抜け出す方法としてコードで実行が使用される場合。 もちろん、これを行わないほうが良いですが、実際にパフォーマンスが向上する場合があります。



そして、実行をやめるにはどれくらいの費用がかかりますか?



さまざまなケースを考えてみてください:彼が自分自身を投げて1つの方法で捕らえられたとき、およびさまざまなスタック深度の状況。



 @Param("1", "2", "10", "100", "1000"}) int depth; @Benchmark public Object throwCatch() { try { return recursive(depth); } catch (Exception e) { return e; } }
      
      





測定値から得られるものは次のとおりです。







つまり 浅い深さがある場合(実行は同じフレームまたはより高いフレーム-深さ0または1でキャッチされます)、実行する価値はありません。 ただし、スタックの深さが大きくなると、コストはまったく異なります。 この場合、明確な線形関係が観察されます。除外の「コスト」は、スタックの深さにほぼ線形に依存します。



スタックトレースを取得するだけでなく、追加の操作(印刷、ネットワーク経由での送信、記録)だけが、保存されたスタックトレースをJavaオブジェクトに変換するgetStackTraceメソッドに使用されます。



 @Benchmark public Object fillInStackTrace() { return new Exception(); } @Benchmark public Object getStackTrace() { return new Exception().getStackTrace(); }
      
      





スタックトレースの変換は、受信の10倍の「高価」であることがわかります。







なぜこれが起こっているのですか?



JDKソースのgetStackTraceメソッドを次に示します。







最初に、ネイティブメソッドを呼び出してスタックの深さを確認し、次にこの深さまでのループでネイティブメソッドを呼び出して次のフレームを取得し、それをStackTraceElementオブジェクトに変換します(これはフィールドの束を持つ通常のJavaオブジェクトです)。 時間が長いだけでなく、プロシージャは多くのメモリを消費します。



さらに、Java 9では、このオブジェクトは(よく知られたモジュール化プロジェクトに関連して)新しいフィールドで補完されます-各フレームには、それがどのモジュールからのものであるかのマークが割り当てられます。







正規表現で解析した人たちにこんにちは。 Java 9で驚きの準備をしてください-モジュールも表示されます。



まとめましょう




いくつかのヒント:






スレッドダンプのスタックトレード



プログラムの動作を確認するための最も簡単な方法は、たとえばjstackユーティリティを使用してスレッドダンプを取得することです。



このユーティリティの出力の断片:







ここに見えるものは何ですか? スレッドとは何ですか、スレッドとその現在のスタックはどのような状態ですか。



さらに、ストリームがロックをキャプチャした場合、同期セクションに入るのを待機している場合、またはReentrantLockを取得する場合、これもスタックトレースに反映されます。



あまり知られていない識別子が役立つ場合があります。







これは、オペレーティングシステムのスレッドIDに直接関連しています。 たとえば、CPUを最も多く消費するスレッドがLinuxのトッププログラムである場合、ストリームのpidはスレッドダンプに表示されるまさにnidです。 一致するJavaスレッドをすぐに見つけることができます。



(同期オブジェクトを使用する)モニターの場合、どのスレッドとどのモニターが保持し、誰がそれらをキャプチャしようとしているのか、スレッドダンプに直接書き込まれます。



ReentrantLockの場合、これは残念ながらそうではありません。 ここでは、スレッド1が特定のReentrantLockをキャプチャしようとしている方法を確認できますが、このロックを保持しているユーザーは表示されません。 この場合、VMにはオプションがあります。



 -XX:+PrintConcurrentLocks
      
      





PrintConcurrentLocksで同じことを実行すると、スレッドダンプにReentrantLockが表示されます。







これが同じロックのIDです。 スレッド2が彼を捕らえたことがわかります。



オプションが非常に優れている場合、「デフォルト」にしないでください。



彼女も何か価値があります。 ReentrantLocksが保持するストリームに関する情報を出力するために、JVMはJavaヒープ全体を実行し、そこですべてのReentrantLocksを探し、それらをスレッドと比較してからこの情報を表示します(スレッドはキャプチャしたロックに関する情報を持ちません。情報は逆方向にのみ-どのロックがどのスレッドに関連付けられているか)。



この例では、スレッドの名前(スレッド1 /スレッド2)は、それらが参照するものを理解していません。 実践からのアドバイス:例えば、サーバーがクライアント要求を処理したり、逆にクライアントが複数のサーバーにアクセスしたりするなど、何らかの長い操作がある場合は、スレッドをフレンドリ名で設定します(以下の場合のように、クライアントが現在のサーバーのIP来る)。 ストリームのダンプでは、応答を待っているサーバーの種類がすぐにわかります。







十分な理論。 練習に戻りましょう。 この例を何度も引用しました。



 package demo2; import java.util.stream.IntStream; public class ParallelSum {   static int SUM = IntStream.range(0, 100).parallel().reduce(0, (x, y) -> x + y);   public static void main(String[] args) {       System.out.println(SUM);   } }
      
      





プログラムを3回連続で実行します。 2回目は0から100までの数字の合計(100を含まない)を表示しますが、3回目は表示しません。 スレッドダンプを見てみましょう。







最初のスレッドは実行可能になり、reduceが実行されます。 しかし、興味深い点です。Thread.StateはRUNNABLEのようなものですが、ストリームはObject.wait()にあると書かれています。







私にとってもはっきりしませんでした。 バグを報告したかったのですが、そのようなバグは何年も前に発生し、「問題ではなく、修正されません」という文言で閉じられていることがわかりました。

このプログラムには本当にデッドロックがあります。 その理由は、 クラスの初期化です。



式は、ParallelSumクラスの静的初期化子で実行されます。



 static int SUM = IntStream.range(0, 100).parallel().reduce(0, (x, y) -> x + y);
      
      





しかし、ストリームは並列であるため、実行は別個のForkJoinPoolスレッドで発生し、そこからラムダ本体が呼び出されます。



 (x, y) -> x + y
      
      





ラムダコードは、JavaコンパイラによってParallelSumクラスにプライベートメソッドとして直接書き込まれます。 ForkJoinPoolから、現在初期化段階にあるParallelSumクラスにアクセスしようとしていることがわかります。 したがって、スレッドはクラスの初期化が終了すると待機を開始し、この畳み込みの計算を予期するため、終了できません。 デッドロック。



最初に金額が考慮されたのはなぜですか? 幸運です。 少数の要素をまとめると、すべてが1つのスレッドで実行される場合があります(もう一方のスレッドには時間がありません)。



しかし、なぜ、RUNNABLEスタックトレースのスレッドなのでしょうか? Thread.Stateのドキュメントを読むと、ここに他の状態が存在しないことが明らかになります。 Javaモニターでスレッドがブロックされていないため、BLOCKED状態はありません。同期セクションはありません。また、ここではObject.wait()の呼び出しがないため、WAITING状態はありません。 同期は、仮想マシンの内部オブジェクトで発生します。一般的に、Javaオブジェクトである必要はありません。



スタックトレースのロギング



状況を想像してください。アプリケーション内の場所の山に何かが記録されます。 この行またはその行がどこから出現したかを知ることは有用でしょう。







Javaにはプリプロセッサがないため、Cのように__FILE __、__ LINE__マクロを使用する方法はありません(これらのマクロは、コンパイル段階で現在のファイル名と文字列に変換されます)。 そのため、スタックトレースを使用する場合を除き、出力をファイル名とコードが出力された行の行番号で補完する他の方法はありません。



 public static String getLocation() { StackTraceElement s = new Exception().getStackTrace()[2]; return s.getFileName() + ':' + s.getLineNumber(); }
      
      





実行を生成し、それからスタックトレースを取得します。この場合、2番目のフレームを取得します(ゼロはgetLocationメソッドで、最初は警告メソッドを呼び出します)。



知っているように、スタックトレースを取得し、特にスタックトレース要素に変換するのは非常に高価です。 そして、1つのフレームが必要です。 どうにかして(実行せずに)簡単ですか?



getStackTraceに加えて、例外にはThreadオブジェクトのgetStackTraceメソッドがあります。



 Thread.current().getStackTrace()
      
      





速くなりますか?







いや JVMは魔法をかけません。ここでは、まったく同じスタックトレースで同じ受信を介してすべてが動作します。



しかし、まだ難しい方法があります。



 public static String getLocation() { StackTraceElement s = sun.misc.SharedSecrets.getJavaLangAccess() .getStackTraceElement(new Exception(), 2); return s.getFileName() + ':' + s.getLineNumber(); }
      
      





Unsafe、SharedSecretsなど、あらゆる種類のプライベートなものが大好きです。



特定のフレームのStackTraceElementを取得できるアクセサがあります(スタックトレース全体をJavaオブジェクトに変換する必要はありません)。 これはより速く動作します。 しかし、悪いニュースがあります:Java 9ではこれは機能しません。 スタックトレースに関連するすべてをリファクタリングするために多くの作業が行われましたが、現在はそのようなメソッドはありません。



単一のフレームを取得できる設計は、いわゆるCaller-sensitiveメソッド、つまり呼び出し元によって結果が異なる可能性のあるメソッドで役立ちます。 アプリケーションでは、このようなメソッドに遭遇することがよくありますが、JDK自体にも同様の例が数多くあります。







誰がClass.forNameを呼び出すかに応じて、対応するクラスローダー(このメソッドを呼び出したクラス)でクラスが検索されます。 同様に、ResourceBundleを取得してSystem.loadLibraryライブラリーをロードします。 また、アクセス許可を確認するさまざまなメソッドを使用する場合(およびこのコードにこのメソッドを呼び出す権利があるかどうか)、誰が呼び出しているかに関する情報が役立ちます。 この場合、「シークレット」APIはgetCallerClassメソッドを提供します。これは実際にはJVM組み込み関数であり、通常はほとんど費用がかかりません。



 sun.reflect.Reflection.getCallerClass
      
      





何度も言われているように、プライベートAPIは悪用することを強く推奨します(以前に呼び出されたUnsafeのような問題に遭遇するリスクがあります)。 したがって、JDK開発者は、これを使用した後、法的代替手段、つまりスレッドをバイパスする新しいAPIが必要になるという事実を考えました。 このAPIの基本要件:





Java 9のパブリックリリースにはjava.lang.StackWalkerが存在することが知られています。

インスタンスの取得は非常に簡単です-getInstanceメソッドを使用します。 いくつかのオプションがあります-デフォルトのStackWalkerまたは少し設定可能なオプション:









また、最適化のために、必要なおおよその深さを設定できます(JVMがバッチでのスタックフレームの受信を最適化できるように)。



これを使用する最も簡単な例:



 StackWalker sw = StackWalker.getInstance(); sw.forEach(System.out::println);
      
      





StackWalkerを取得し、forEachメソッドを呼び出して、すべてのフレームをバイパスします。 その結果、次のような単純なスタックトレースを取得します。







SHOW_REFLECT_FRAMESオプションでも同じことが言えます。



 StackWalker sw = StackWalker.getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES); sw.forEach(System.out::println);
      
      





この場合、リフレクションを介した呼び出しに関連するメソッドが追加されます。







SHOW_HIDDEN_FRAMESオプションを追加する場合(ちなみに、SHOW_REFLECT_FRAMESが含まれます。つまり、反射フレームも表示されます):



 StackWalker sw = StackWalker.getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES); sw.forEach(System.out::println);
      
      





動的に生成されたラムダクラスのメソッドは、スタックトレースに表示されます。







そして今、StackWalker APIの主なメソッドは、多数のジェネリックを持つこのようなcで曖昧な署名を持つwalkメソッドです。



 public <T> T walk(Function<? super Stream<StackFrame>, ? extends T> function)
      
      





walkメソッドは、スタックフレームから関数を取得します。



彼の作品は例で簡単に見せることができます。



それはすべて怖いように見えるという事実にもかかわらず、それを使用する方法は明らかです。 ストリームは関数に転送され、すでにストリーム上ですべての通常の操作を実行できます。 たとえば、getCallerFrameメソッドは次のようになり、2番目のフレームのみを取得します。最初の2つがスキップされ、findFirstが呼び出されます。



 public static StackFrame getCallerFrame() { return StackWalker.getInstance() .walk(stream -> stream.skip(2).findFirst()) .orElseThrow(NoSuchElementException::new); }
      
      





walkメソッドは、このストリーム関数が返す結果を返します。 すべてがシンプルです。

この特定の場合(Callerクラスを取得する必要がある場合)には、特別なショートカットメソッドがあります。



 return StackWalker.getInstance(RETAIN_CLASS_REFERENCE).getCallerClass();
      
      





別の例はより複雑です。



すべてのフレームを調べて、org.apacheパッケージに属するフレームのみを残し、リストの最初の10個を表示します。



 StackWalker sw = StackWalker.getInstance(); List<StackFrame> frames = sw.walk(stream -> stream.filter(sf -> sf.getClassName().startsWith("org.apache.")) .limit(10) .collect(Collectors.toList()));
      
      





興味深い質問:なぜ多くのジェネリックを含む長い署名なのか? StackWalkerをストリームを返すメソッドにしないのはなぜですか?



 public Stream<StackFrame> stream();
      
      





ストリームを返すAPIを指定すると、JDKはこのストリームで行われていることに対する制御を失います。 このストリームをさらにどこかに配置し、別のスレッドに渡して、受信の2時間後に使用しようとすることができます(バイパスしようとしたスタックは長い間失われ、スレッドは長時間強制終了されます)。 したがって、Stack Walker APIの「遅延」を保証することは不可能です。



Stack Walker APIの主なポイント:ウォーク内にいる間、スタックの状態は固定されているため、このスタックに対するすべての操作は遅延して実行できます。



デザートについては、もう少し興味深い。



いつものように、JDK開発者は私たちからたくさんの宝を隠しています。 また、通常のスタックフレームに加えて、一部のニーズのために、メソッドとクラスに関する情報だけでなく、モニターやこのスタックフレームのエクスプレススタックの値。



 /* package-private */ interface LiveStackFrame extends StackFrame { public Object[] getMonitors(); public Object[] getLocals(); public Object[] getStack(); public static StackWalker getStackWalker(); }
      
      





ここでの保護はそれほど熱くありません。クラスは単に非公開になりました。 しかし、誰が私たちを反省し、それを試すのを妨げているのでしょうか? (注:現在のJDK 9ビルドでは、リフレクションによる非パブリックAPIへのアクセスは禁止されています。有効にするには、JVMオプション--add-opens=java.base/java.lang=ALL-UNNAMED



追加する必要があり--add-opens=java.base/java.lang=ALL-UNNAMED







そのような例を試してみます。 迷路から抜け出す方法を再帰的に検索するプログラムがあります。 四角い箱のサイズxサイズがあります。 現在の座標を持つvisitメソッドがあります。 現在のセルから左/右/上/下に移動しようとしています(ビジーでない場合)。 右下のセルから左上のセルに移動する場合は、スタックを見つけて印刷する方法を見つけたと思われます。



 package demo3; import java.util.Random; public class Labyrinth {   static final byte FREE = 0;   static final byte OCCUPIED = 1;   static final byte VISITED = 2;   private final byte[][] field;   public Labyrinth(int size) {       Random random = new Random(0);       field = new byte[size][size];       for (int x = 0; x < size; x++) {           for (int y = 0; y < size; y++) {               if (random.nextInt(10) > 7) {                   field[x][y] = OCCUPIED;               }           }       }       field[0][0] = field[size - 1][size - 1] = FREE;   }   public int size() {       return field.length;   }   public boolean visit(int x, int y) {       if (x == 0 && y == 0) {           StackTrace.dump();           return true;       }       if (x < 0 || x >= size() || y < 0 || y >= size() || field[x][y] != FREE) {           return false;       }       field[x][y] = VISITED;       return visit(x - 1, y) || visit(x, y - 1) || visit(x + 1, y) || visit(x, y + 1);   }   public String toString() {       return "Labyrinth";   }   public static void main(String[] args) {       Labyrinth lab = new Labyrinth(10);       boolean exitFound = lab.visit(9, 9);       System.out.println(exitFound);   } }
      
      





以下を開始します。







すでにJava 8に含まれていた通常のdumpStackを実行すると、何も明確ではない通常のスタックトレースが取得されます。 明らかに、再帰的なメソッドはそれ自体を呼び出しますが、各メソッドがどのステップ(およびどの座標値)で呼び出されるかは興味深いです。



標準のdumpStackをStackTrace.dumpに置き換えます。これは、リフレクションを通じてライブスタックフレームを使用します。



 package demo3; import java.lang.reflect.Method; import java.util.Arrays; public class StackTrace {   private static Object invoke(String methodName, Object instance) {       try {           Class<?> liveStackFrame = Class.forName("java.lang.LiveStackFrame");           Method m = liveStackFrame.getMethod(methodName);           m.setAccessible(true);           return m.invoke(instance);       } catch (ReflectiveOperationException e) {           throw new AssertionError("Should not happen", e);       }   }   public static void dump() {       StackWalker sw = (StackWalker) invoke("getStackWalker", null);       sw.forEach(frame -> {           Object[] locals = (Object[]) invoke("getLocals", frame);           System.out.println(" at " + frame + "  " + Arrays.toString(locals));       });   } }
      
      





まず、getStackWalkerメソッドを呼び出して、適切なStackWalkerを取得する必要があります。getStackWalkerに転送されるすべてのフレームは、実際には追加のメソッド、特にローカル変数を取得するためのgetLocalsを持つライブスタックフレームのインスタンスになります。



始めます。 同じことを取得しますが、迷路からのパス全体がローカル変数の値の形式で表示されます。










. .



— 7-8 JPoint 2017 . « JVM- », , , . «» , !



, JPoint Java — , .



All Articles