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
方法ポイントの行番号情報o
がbar()
メソッドへの引数の一つの捕獲である自由変数である
() -> o.hashCode()
あります。
これはキャッチです。あなたが言ったように、ラムダクラス名の数字は、ランタイム生成順のシーケンス番号です。したがって、実際のラムダ式へのマッピングは、ラムダ式の正確な評価順序に依存します。ラムダ式は、複雑なプログラムの実行が異なる場合があります。 @Holger右。 – Holger
大きなオブジェクトをキャプチャしたインスタンスは 'CaptureTest $$ Lambda $ 9'ではないかもしれません。それは例えば 'CaptureTest $$ Lambda $ 347'かもしれません。ヒープダンプはどのクラスかを示します。そのクラスを取得したら、それを逆アセンブルして呼び出す静的メソッドを見つけることができます。その静的なメソッドは、ソースコードにさかのぼることができます。ラムダ静的メソッドの命名規則は指定されていませんが、安定しており、かなり予測可能です。 –
@Holger静的メソッド逆アセンブリの行番号テーブルは、ソースファイル内の正しい場所を指しています。私はこれを含める答えを編集しました。 –