从堆转储中的损坏名称中查找 Java lambda
Finding a Java lambda from its mangled name in a heap dump
我正在寻找内存泄漏,堆转储显示许多 lambda 实例持有有问题的对象。 lambda 的名称是周围的 class 名称,最后是 $$lambda7
。我还可以看到它有一个字段(它是它的正确名称),称为 arg
,它引用填充堆的对象。不幸的是,我在这个 class 中有很多 lambda,我想知道我可以做些什么来缩小它的范围。
我假设 arg
是一个隐式参数——lambda 表达式中的一个自由变量,当 lambda 成为闭包时被捕获。那是对的吗?
我也猜想 107 孤立起来并没有真正的帮助,但是我可以设置一些标志来记录哪个 lambda 表达式得到什么数字吗?
还有其他有用的提示吗?
这个数字非常无用,因为它是在运行时按照所述 class 中每个 Lambda 创建(遇到)的顺序确定的——如果这是正确的,那么你在那个 class 中有超过 100 个 Lambda =15=]。参见
如果您不能根据它所引用的内容限制对有问题的 lambda 的调查,那么您最好的选择是稍微简化 class 或将一些最明显的嫌疑人转换为方法引用。
这个有点绕,不过你可以试试:
用 -Djdk.internal.lambda.dumpProxyClasses=/path/to/directory/
启动 JVM。该选项将使 JVM 将生成的代理对象(class 文件)转储到您选择的目录
你可以尝试反编译生成的classes。我创建了一个使用 lambda 的示例 Java 代码,然后在 Intellij Idea 中打开生成的 class 文件之一(文件名为 Test$$Lambda$3.class),它已被反编译为:
import java.util.function.IntPredicate;
// $FF: synthetic class
final class Test$$Lambda implements IntPredicate {
private Test$$Lambda() {
}
public boolean test(int var1) {
return Test.lambda$bar(var1);
}
}
从那里你可以推断出 lambda 的类型(示例中的 IntPredicate
),class 的名称,它是在(Test
)中定义的,以及它在 (bar
).
中定义的方法
OP 的猜想是正确的,arg
是包含捕获值的 lambda 对象的字段。 的答案是正确的,让 lambda 元工厂转储其代理 classes。 (+1)
这是一种使用 javap
工具来跟踪将引用返回到源代码的实例的方法。基本上你找到合适的代理 class;反汇编它以找出它调用了哪个合成 lambda 方法;然后将该合成 lambda 方法与源代码中的特定 lambda 表达式相关联。
(大多数(如果不是全部)信息适用于 Oracle JDK 和 OpenJDK。它可能不适用于不同的 JDK 实现。此外,这取决于将来会发生变化。这应该适用于任何最新的 Oracle JDK 8 或 OpenJDK 8。它可能会继续在 JDK 9 中工作。)
首先,介绍一下背景。编译包含 lambda 的源文件时,javac
会将 lambda 主体编译为驻留在包含 class 中的合成方法。这些方法是私有和静态的,它们的名称类似于 lambda$<method>$<count>
,其中 method 是包含 lambda 的方法的名称,count 是一个顺序计数器,从源文件的开头(从零开始)对方法进行编号。
当 lambda 表达式在 运行 时首次 求值 时,将调用 lambda 元工厂。这会生成一个 class 来实现 lambda 的功能接口。它实例化此 class,将参数传递给功能接口方法(如果有),将它们与任何捕获的值组合,并调用由 javac
编译的合成方法,如上所述。此实例称为 "function object" 或 "proxy".
通过让 lambda 元工厂转储其代理 classes,您可以使用 javap
反汇编字节码并将代理实例追溯到生成它的 lambda 表达式。这可能最好用一个例子来说明。考虑以下代码:
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 正在执行捕获?
首先要做的是编译此 class 并编译为 运行 javap -v -p CaptureTest
。 -v
选项显示反汇编的字节码和其他信息,例如行号表。必须提供 -p
选项才能使 javap
反汇编私有方法。它的输出包括很多东西,但重要的部分是合成 lambda 方法:
private static int lambda$bar(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(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(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[=11=]();
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 中的每一个生成的。
然后,运行 内存分析器下的程序,向 java
命令提供命令行参数 -Djdk.internal.lambda.dumpProxyClasses=<outputdir>
。这会导致 lambda 元工厂将其生成的 classes 转储到指定目录(必须已经存在)。
获取应用程序的内存配置文件并进行检查。有多种方法可以做到这一点;我使用了 NetBeans 内存分析器。当我 运行 它时,它告诉我一个包含 10,000,000 个元素的 byte[] 被一个名为 CaptureTest$$Lambda
的 class 中的字段 arg
保存。这是 OP 得到的最大程度。
此 class 名称上的计数器没有用,因为它表示由 lambda 元工厂生成的 classes 的序列号,按照它们在 [=91= 处生成的顺序]时间。知道 运行 时间序列并不能告诉我们它在源代码中的起源位置。
但是,我们已经要求 lambda 元工厂转储它的 classes,所以我们可以去看看这个特定的 class 看看它做了什么。确实,在输出目录中,有一个文件CaptureTest$$Lambda.class
。 运行 javap -c
上面显示了以下内容:
final class CaptureTest$$Lambda implements java.util.function.IntSupplier {
public int getAsInt();
Code:
0: aload_0
1: getfield #15 // Field arg:Ljava/lang/Object;
4: invokestatic #28 // Method CaptureTest.lambda$bar:(Ljava/lang/Object;)I
7: ireturn
}
您可以反编译常量池条目,但 javap
有助于将符号名称放在字节码右侧的注释中。您可以看到这加载了 arg
字段——有问题的引用——并将它传递给方法 CaptureTest.lambda$bar
。这是源文件中的第 2 个 lambda(从零开始),它是 bar()
方法中两个 lambda 表达式中的第一个。现在您可以返回到原始 class 的 javap
输出并使用来自 lambda 静态方法的行号信息来查找源文件中的位置。 CaptureTest.lambda$bar
方法的行号信息指向第29行,这个位置的lambda为
() -> o.hashCode()
其中 o
是一个自由变量,它是 bar()
方法的参数之一的捕获。
我正在寻找内存泄漏,堆转储显示许多 lambda 实例持有有问题的对象。 lambda 的名称是周围的 class 名称,最后是 $$lambda7
。我还可以看到它有一个字段(它是它的正确名称),称为 arg
,它引用填充堆的对象。不幸的是,我在这个 class 中有很多 lambda,我想知道我可以做些什么来缩小它的范围。
我假设 arg
是一个隐式参数——lambda 表达式中的一个自由变量,当 lambda 成为闭包时被捕获。那是对的吗?
我也猜想 107 孤立起来并没有真正的帮助,但是我可以设置一些标志来记录哪个 lambda 表达式得到什么数字吗?
还有其他有用的提示吗?
这个数字非常无用,因为它是在运行时按照所述 class 中每个 Lambda 创建(遇到)的顺序确定的——如果这是正确的,那么你在那个 class 中有超过 100 个 Lambda =15=]。参见
如果您不能根据它所引用的内容限制对有问题的 lambda 的调查,那么您最好的选择是稍微简化 class 或将一些最明显的嫌疑人转换为方法引用。
这个有点绕,不过你可以试试:
用
-Djdk.internal.lambda.dumpProxyClasses=/path/to/directory/
启动 JVM。该选项将使 JVM 将生成的代理对象(class 文件)转储到您选择的目录你可以尝试反编译生成的classes。我创建了一个使用 lambda 的示例 Java 代码,然后在 Intellij Idea 中打开生成的 class 文件之一(文件名为 Test$$Lambda$3.class),它已被反编译为:
import java.util.function.IntPredicate; // $FF: synthetic class final class Test$$Lambda implements IntPredicate { private Test$$Lambda() { } public boolean test(int var1) { return Test.lambda$bar(var1); } }
从那里你可以推断出 lambda 的类型(示例中的
IntPredicate
),class 的名称,它是在(Test
)中定义的,以及它在 (bar
). 中定义的方法
OP 的猜想是正确的,arg
是包含捕获值的 lambda 对象的字段。
这是一种使用 javap
工具来跟踪将引用返回到源代码的实例的方法。基本上你找到合适的代理 class;反汇编它以找出它调用了哪个合成 lambda 方法;然后将该合成 lambda 方法与源代码中的特定 lambda 表达式相关联。
(大多数(如果不是全部)信息适用于 Oracle JDK 和 OpenJDK。它可能不适用于不同的 JDK 实现。此外,这取决于将来会发生变化。这应该适用于任何最新的 Oracle JDK 8 或 OpenJDK 8。它可能会继续在 JDK 9 中工作。)
首先,介绍一下背景。编译包含 lambda 的源文件时,javac
会将 lambda 主体编译为驻留在包含 class 中的合成方法。这些方法是私有和静态的,它们的名称类似于 lambda$<method>$<count>
,其中 method 是包含 lambda 的方法的名称,count 是一个顺序计数器,从源文件的开头(从零开始)对方法进行编号。
当 lambda 表达式在 运行 时首次 求值 时,将调用 lambda 元工厂。这会生成一个 class 来实现 lambda 的功能接口。它实例化此 class,将参数传递给功能接口方法(如果有),将它们与任何捕获的值组合,并调用由 javac
编译的合成方法,如上所述。此实例称为 "function object" 或 "proxy".
通过让 lambda 元工厂转储其代理 classes,您可以使用 javap
反汇编字节码并将代理实例追溯到生成它的 lambda 表达式。这可能最好用一个例子来说明。考虑以下代码:
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 正在执行捕获?
首先要做的是编译此 class 并编译为 运行 javap -v -p CaptureTest
。 -v
选项显示反汇编的字节码和其他信息,例如行号表。必须提供 -p
选项才能使 javap
反汇编私有方法。它的输出包括很多东西,但重要的部分是合成 lambda 方法:
private static int lambda$bar(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(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(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[=11=]();
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 中的每一个生成的。
然后,运行 内存分析器下的程序,向 java
命令提供命令行参数 -Djdk.internal.lambda.dumpProxyClasses=<outputdir>
。这会导致 lambda 元工厂将其生成的 classes 转储到指定目录(必须已经存在)。
获取应用程序的内存配置文件并进行检查。有多种方法可以做到这一点;我使用了 NetBeans 内存分析器。当我 运行 它时,它告诉我一个包含 10,000,000 个元素的 byte[] 被一个名为 CaptureTest$$Lambda
的 class 中的字段 arg
保存。这是 OP 得到的最大程度。
此 class 名称上的计数器没有用,因为它表示由 lambda 元工厂生成的 classes 的序列号,按照它们在 [=91= 处生成的顺序]时间。知道 运行 时间序列并不能告诉我们它在源代码中的起源位置。
但是,我们已经要求 lambda 元工厂转储它的 classes,所以我们可以去看看这个特定的 class 看看它做了什么。确实,在输出目录中,有一个文件CaptureTest$$Lambda.class
。 运行 javap -c
上面显示了以下内容:
final class CaptureTest$$Lambda implements java.util.function.IntSupplier {
public int getAsInt();
Code:
0: aload_0
1: getfield #15 // Field arg:Ljava/lang/Object;
4: invokestatic #28 // Method CaptureTest.lambda$bar:(Ljava/lang/Object;)I
7: ireturn
}
您可以反编译常量池条目,但 javap
有助于将符号名称放在字节码右侧的注释中。您可以看到这加载了 arg
字段——有问题的引用——并将它传递给方法 CaptureTest.lambda$bar
。这是源文件中的第 2 个 lambda(从零开始),它是 bar()
方法中两个 lambda 表达式中的第一个。现在您可以返回到原始 class 的 javap
输出并使用来自 lambda 静态方法的行号信息来查找源文件中的位置。 CaptureTest.lambda$bar
方法的行号信息指向第29行,这个位置的lambda为
() -> o.hashCode()
其中 o
是一个自由变量,它是 bar()
方法的参数之一的捕获。