OP 的猜想是正确的,即 lambda 对象的字段包含捕获的值。来自 lukeg 的答案是正确的,让 lambda 元工厂转储其代理类。(+1)arg$1
下面是一种方法,它使用该工具跟踪将引用保留回源代码的实例。基本上,您找到了正确的代理类;反汇编它以找出它调用的合成lambda方法;然后将该合成 lambda 方法与源代码中的特定 lambda 表达式相关联。javap
(大多数(如果不是全部)这些信息适用于Oracle JDK和OpenJDK。它可能不适用于不同的 JDK 实现。此外,这在未来可能会发生变化。不过,这应该适用于任何最近的Oracle JDK 8或OpenJDK 8。它可能会继续在JDK 9中工作。
首先,介绍一些背景知识。编译包含 lambda 的源文件时,会将 lambda 主体编译为驻留在包含类中的合成方法。这些方法是私有的和静态的,它们的名称类似于 method 是包含 lambda 的方法的名称,而 count 是一个顺序计数器,它从源文件的开头(从零开始)对方法进行编号。javac
lambda$<method>$<count>
在运行时首次计算 lambda 表达式时,将调用 lambda 元工厂。这将生成一个实现 lambda 函数接口的类。它实例化此类,将参数转换为函数接口方法(如果有),将它们与任何捕获的值组合在一起,并调用由 编译的合成方法,如上所述。此实例称为“函数对象”或“代理”。javac
通过让 lambda 元工厂转储其代理类,您可以使用 来反汇编字节码,并将代理实例追溯到为其生成它的 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
}
}
此代码分配一个大数组,然后计算四个不同的 lambda 表达式。其中之一捕获对大型数组的引用。(如果你知道要找什么,你可以通过检查来判断,但有时这很难。哪个 lambda 正在执行捕获?
首先要做的是编译这个类并运行 。该选项显示已拆卸的字节码和其他信息,如行号表。必须提供该选项才能反汇编私有方法。这个输出包括很多东西,但重要的部分是合成的lambda方法:javap -v -p CaptureTest
-v
-p
javap
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
方法名称末尾的计数器从零开始,从文件开头开始按顺序编号。此外,综合方法名称包括包含 lambda 表达式的方法的名称,因此我们可以分辨出哪个方法是从单个方法中出现的多个 lambda 中的每个 lambda 生成的。
然后,在内存探查器下运行该程序,并向该命令提供命令行参数。这会导致 lambda 元工厂将其生成的类转储到命名目录(该目录必须已经存在)。-Djdk.internal.lambda.dumpProxyClasses=<outputdir>
java
获取应用程序的内存配置文件并进行检查。有多种方法可以做到这一点;我使用了 NetBeans 内存探查器。当我运行它时,它告诉我,一个包含10,000,000个元素的字节[]由名为 的类中的字段持有。这是OP所得到的。arg$1
CaptureTest$$Lambda$9
这个类名上的计数器没有用,因为它表示 lambda 元工厂生成的类的序列号,按照它们在运行时生成的顺序。了解运行时序列并不能告诉我们很多关于它在源代码中的起源。
但是,我们已经要求 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
}
您可以反编译常量池条目,但可以很有帮助地将符号名称放在字节码右侧的注释中。您可以看到,这将加载字段 (有问题的引用)并将其传递给方法 。这是源文件中的 lambda 编号 2(从零开始),它是该方法中两个 lambda 表达式中的第一个。现在,您可以返回到原始类的输出,并使用 lambda 静态方法中的行号信息在源文件中查找位置。该方法的行号信息指向第 29 行。此位置的 lambda 为javap
arg$1
CaptureTest.lambda$bar$2
bar()
javap
CaptureTest.lambda$bar$2
() -> o.hashCode()
其中 是一个自由变量,它是对方法的一个参数的捕获。o
bar()