从堆转储中的损坏名称中查找 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() 方法的参数之一的捕获。