2017-01-10 2 views
7

私はメモリリークを探しています。ヒープダンプは、問題のオブジェクトを保持しているいくつかのラムダインスタンスを示しています。ラムダの名前は末尾に$$lambda$107の周囲のクラス名です。また、ヒープを埋めるオブジェクトを参照するarg$1と呼ばれる1つのフィールド(それはそれに適した名前)があることがわかります。残念ながら、このクラスにはかなりのラムダがあり、私はそれを絞り込むために何ができるのだろうかと思います。ヒープダンプ内のマングルされた名前からJavaラムダを見つける

私はarg$1が暗黙の引数であると仮定しています - ラムダがクロージャになるときに取得されるラムダ式の自由変数です。あれは正しいですか?

私は107が本当に助けになるわけではないと推測していますが、ラムダ式がどのような数になるのかを記録するためのフラグがいくつかありますか?

その他の便利なヒント?

答えて

5

OPの推測は、arg$1がキャプチャされた値を含むラムダオブジェクトのフィールドであることが正しいことを示しています。 lukegからの答えはラムダメタファクトがプロキシクラスをダンプするのを正しい軌道上にしています。 (+1)

javapツールを使用して、参照をソースコードに戻しているインスタンスを追跡する方法は次のとおりです。基本的に正しいプロキシクラスを見つけることができます。それを分解して、どの合成ラムダ法を呼び出すかを調べる。その合成ラムダメソッドをソースコード中の特定のラムダ式に関連付ける。

(ほとんどの場合、この情報のすべてではありませんが、Oracle JDKおよびOpenJDKに適用されます)異なるJDK実装では機能しない可能性があります。 JDK 9では引き続き動作します)。

まず、少し背景があります。ラムダを含むソースファイルがコンパイルされると、javacは、ラムダボディを、そのクラスを含む合成メソッドにコンパイルします。これらのメソッドはプライベートで静的で、名前はlambda$<method>$<count>のようになります。のメソッドはラムダを含むメソッドの名前です。のカウントは、ソースファイルの先頭からメソッドに番号を付ける逐次カウンタですゼロから)。

ラムダ式が最初にと評価された場合、実行時にと評価され、ラムダメタファクトが呼び出されます。これにより、ラムダの機能インタフェースを実装するクラスが生成されます。このクラスをインスタンス化し、関数インタフェースメソッド(存在する場合)に引数を取り込み、取得した値とそれらを結合し、上述のようにjavacによってコンパイルされた合成メソッドを呼び出します。このインスタンスは、「関数オブジェクト」または「プロキシ」と呼ばれます。

lambdaメタファクトをプロキシクラスをダンプするように設定すると、javapを使用してバイトコードを逆アセンブルし、プロキシインスタンスを生成したラムダ式に戻すことができます。これはおそらく例で最もよく説明されています。

public class CaptureTest { 
    static List<IntSupplier> list; 

    static IntSupplier foo(boolean b, Object o) { 
     if (b) { 
      return() -> 0;      // line 20 
     } else { 
      int h = o.hashCode(); 
      return() -> h;      // line 23 
     } 
    } 

    static IntSupplier bar(boolean b, Object o) { 
     if (b) { 
      return() -> o.hashCode();   // line 29 
     } else { 
      int len = o.toString().length(); 
      return() -> len;     // line 32 
     } 
    } 

    static void run() { 
     Object big = new byte[10_000_000]; 

     list = Arrays.asList(
      bar(false, big), 
      bar(true, big), 
      foo(false, big), 
      foo(true, big)); 

     System.out.println("Done."); 
    } 

    public static void main(String[] args) throws InterruptedException { 
     run(); 
     Thread.sleep(Long.MAX_VALUE); // stay alive so a heap dump can be taken 
    } 
} 

このコードは、大きな配列を割り当て、次に4つの異なるラムダ式を評価します。これらのうちの1つは、大きな配列への参照を取得します。 (あなたが探しているものが分かっているかどうかは検査で知ることができますが、時にはこれは難しいこともあります)。どのラムダがキャプチャをしていますか?

最初に行うことは、このクラスをコンパイルしてjavap -v -p CaptureTestを実行することです。 -vオプションは、逆アセンブルされたバイトコードと、行番号表などのその他の情報を表示します。プライベートメソッドを逆アセンブルするためにjavapを取得するには、-pオプションを指定する必要があります。これの出力は多くのものが含まれていますが、重要な部分は、合成ラムダメソッドです:

private static int lambda$bar$3(int); 
    descriptor: (I)I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=1, args_size=1 
     0: iload_0 
     1: ireturn 
    LineNumberTable: 
     line 32: 0 
    LocalVariableTable: 
     Start Length Slot Name Signature 
      0  2  0 len I 

private static int lambda$bar$2(java.lang.Object); 
    descriptor: (Ljava/lang/Object;)I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=1, args_size=1 
     0: aload_0 
     1: invokevirtual #3     // Method java/lang/Object.hashCode:()I 
     4: ireturn 
    LineNumberTable: 
     line 29: 0 
    LocalVariableTable: 
     Start Length Slot Name Signature 
      0  5  0  o Ljava/lang/Object; 

private static int lambda$foo$1(int); 
    descriptor: (I)I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=1, args_size=1 
     0: iload_0 
     1: ireturn 
    LineNumberTable: 
     line 23: 0 
    LocalVariableTable: 
     Start Length Slot Name Signature 
      0  2  0  h I 

private static int lambda$foo$0(); 
    descriptor:()I 
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 
    Code: 
    stack=1, locals=0, args_size=0 
     0: iconst_0 
     1: ireturn 
    LineNumberTable: 
     line 20: 0 

メソッド名の末尾にカウンターがゼロから始まり、ファイルの先頭から順に番号が付けられています。さらに、合成メソッド名にはラムダ式を含むメソッドの名前が含まれているので、単一のメソッド内で発生する複数のラムダのそれぞれからどのメソッドが生成されたかを知ることができます。

メモリプロファイラでプログラムを実行し、コマンドライン引数-Djdk.internal.lambda.dumpProxyClasses=<outputdir>javaコマンドに指定して実行します。これにより、lambdaメタファクトは生成されたクラスを指定されたディレクトリ(既に存在していなければならない)にダンプします。

アプリケーションのメモリプロファイルを取得して検査します。これを行うにはさまざまな方法があります。私はNetBeansメモリプロファイラを使用しました。私はそれを実行したとき、10,000,000要素のバイト[arg$1]が、CaptureTest$$Lambda$9という名前のクラスに保持されていることを教えてくれました。これは、OPが得た限りです。

このクラス名のカウンタは、実行時に生成された順序で、ラムダメタファクトによって生成されたクラスのシーケンス番号を表すので、有用ではありません。ランタイムシーケンスを知っていても、それがソースコード内でどこから発生したかはわかりません。

しかし、lambdaメタファクトにクラスをダンプするように依頼したので、この特定のクラスを見て、それが何かを見ることができます。実際、出力ディレクトリには、ファイルCaptureTest$$Lambda$9.classがあります。それにjavap -cを実行すると、以下のことを明らかに:

final class CaptureTest$$Lambda$9 implements java.util.function.IntSupplier { 
    public int getAsInt(); 
    Code: 
     0: aload_0 
     1: getfield  #15     // Field arg$1:Ljava/lang/Object; 
     4: invokestatic #28     // Method CaptureTest.lambda$bar$2:(Ljava/lang/Object;)I 
     7: ireturn 
} 

あなたは定数プールエントリを逆コンパイルすることができますが、javapは親切バイトコードの右側のコメントでシンボル名を置きます。これにより、arg$1フィールド(問題のある参照)がロードされ、メソッドCaptureTest.lambda$bar$2に渡されることがわかります。これは、ソースファイル内のラムダ2(ゼロから始まる)であり、bar()メソッド内の2つのラムダ式のうちの最初のものです。これで元のクラスのjavap出力に戻り、ラムダ静的メソッドの行番号情報を使用してソースファイル内の場所を見つけることができます。この位置で29ラムダ線にCaptureTest.lambda$bar$2方法ポイントの行番号情報obar()メソッドへの引数の一つの捕獲である自由変数である

() -> o.hashCode() 

あります。

+0

これはキャッチです。あなたが言ったように、ラムダクラス名の数字は、ランタイム生成順のシーケンス番号です。したがって、実際のラムダ式へのマッピングは、ラムダ式の正確な評価順序に依存します。ラムダ式は、複雑なプログラムの実行が異なる場合があります。 @Holger右。 – Holger

+0

大きなオブジェクトをキャプチャしたインスタンスは 'CaptureTest $$ Lambda $ 9'ではないかもしれません。それは例えば 'CaptureTest $$ Lambda $ 347'かもしれません。ヒープダンプはどのクラスかを示します。そのクラスを取得したら、それを逆アセンブルして呼び出す静的メソッドを見つけることができます。その静的なメソッドは、ソースコードにさかのぼることができます。ラムダ静的メソッドの命名規則は指定されていませんが、安定しており、かなり予測可能です。 –

+0

@Holger静的メソッド逆アセンブリの行番号テーブルは、ソースファイル内の正しい場所を指しています。私はこれを含める答えを編集しました。 –

0

実行時に、そのクラスの各ラムダが作成された(遭遇した)順番で決定されるので、この数はかなり無駄です。正しい場合は、その単一のクラスに100個以上のラムダがあります。 What does $$ in javac generated name mean?

参照する内容に基づいて問題のラムダの調査を制限できない場合は、クラスを少し簡略化するか、最も目障りな容疑者の一部をメソッド参照に変換することをお勧めします。

3

これは少し複雑ですが、あなたは試すことがあります。

  • -Djdk.internal.lambda.dumpProxyClasses=/path/to/directory/であなたのJVMを開始します。このオプションを使用すると、生成されたクラスを逆コンパイルすることができます。

  • このように生成されたクラスをJVMダンプによって生成されたプロキシオブジェクト(クラスファイル)に変換します。私は、ラムダを使用するサンプルJavaコードを作成し、その後のIntelliJアイデアで生成されたクラスファイル(テスト$$ラムダの$ 3.classという名前のファイル)のいずれかを開いて、それがに逆コンパイルされていている。そこから

    import java.util.function.IntPredicate; 
    
    // $FF: synthetic class 
    final class Test$$Lambda$3 implements IntPredicate { 
        private Test$$Lambda$3() { 
        } 
    
        public boolean test(int var1) { 
         return Test.lambda$bar$1(var1); 
        } 
    } 
    
  • ラムダの型(例ではIntPredicate)、(Test)で定義されたクラスの名前、(bar)で定義されたメソッドの名前を推論することができます。

関連する問題