从堆转储中的残缺名称中查找 Java lambda

2022-09-03 14:13:54

我正在搜寻内存泄漏,堆转储显示许多 lambda 实例正在保存有问题的对象。lambda 的名称是末尾的周围类名。我还可以看到它有一个字段(它是它的正确名称),称为引用填充堆的对象。不幸的是,我在这个类中有很多lambda,我想知道我能做些什么来缩小它的范围。$$lambda$107arg$1

我假设是一个隐式参数 - lambda表达式中的一个自由变量,当lambda成为闭包时,它会被捕获。这是对的吗?arg$1

我也猜测107在孤立的情况下没有真正的帮助,但是我可以设置一些标志来记录哪个lambda表达式得到什么数字?

还有其他有用的提示吗?


答案 1

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 是一个顺序计数器,它从源文件的开头(从零开始)对方法进行编号。javaclambda$<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-pjavap

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$1CaptureTest$$Lambda$9

这个类名上的计数器没有用,因为它表示 lambda 元工厂生成的类的序列号,按照它们在运行时生成的顺序。了解运行时序列并不能告诉我们很多关于它在源代码中的起源。

但是,我们已经要求 lambda 元工厂转储其类,因此我们可以查看此特定类以了解其功能。实际上,在输出目录中,有一个 文件 。在它上面运行显示以下内容:CaptureTest$$Lambda$9.classjavap -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 为javaparg$1CaptureTest.lambda$bar$2bar()javapCaptureTest.lambda$bar$2

    () -> o.hashCode()

其中 是一个自由变量,它是对方法的一个参数的捕获。obar()


答案 2

这有点复杂,但你可以试试:

  • 使用 启动 JVM。该选项将使JVM转储生成的代理对象(类文件)到您选择的目录-Djdk.internal.lambda.dumpProxyClasses=/path/to/directory/

  • 你可以尝试反编译这样生成的类。我创建了一个使用lambda的示例Java代码,然后在Intellij Idea中打开了一个生成的类文件(名为Test$$Lambda$ 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);
        }
    }
    
  • 从那里,您可以推断出lambda的类型(在示例中),在()中定义的类的名称以及在()中定义它的方法的名称。IntPredicateTestbar