我正在寻找内存泄漏,堆转储显示我有许多lambda实例正在保存有问题的对象.lambda的名称是最后的周围类名$$lambda$107
.我还可以看到它有一个字段(它是正确的名称),调用arg$1
它引用填充堆的对象.不幸的是,我在这堂课中有很多lambdas,我想知道我能做些什么来缩小范围.
我假设arg$1
是一个隐式参数 - lambda表达式中的一个自由变量,当lambda成为一个闭包时被捕获.那是对的吗?
我也猜测107在孤立中没有真正的帮助,但是我可以设置一些标志来记录哪个lambda表达式得到什么数字?
还有其他有用的提示?
OP的猜想是正确的,它arg$1
是包含捕获值的lambda对象的字段.lukeg的答案是在正确的轨道上,让lambda metafactory转储其代理类.(1)
这是一种使用该javap
工具跟踪将引用保存回源代码的实例的方法.基本上你找到合适的代理类; 反汇编它以找出它所调用的合成lambda方法; 然后将该合成lambda方法与源代码中的特定lambda表达式相关联.
(大多数,如果不是所有这些信息,都适用于Oracle JDK和OpenJDK.它可能不适用于不同的JDK实现.此外,这可能会在未来发生变化.这应该适用于任何最新的Oracle JDK 8或OpenJDK 8,虽然.它可能会继续在JDK 9中工作.)
首先,有点背景.编译包含lambdas的源文件时,javac
会将lambda主体编译为驻留在包含类中的合成方法.这些方法是私有的和静态的,它们的名称类似于lambda$
where 方法是包含lambda的方法的名称,count是一个顺序计数器,它对源文件开头的方法进行编号(从零开始).
在运行时首次计算 lambda表达式时,将调用lambda metafactory.这产生了一个实现lambda功能接口的类.它实例化该类,将参数带入功能接口方法(如果有的话),将它们与任何捕获的值组合,并调用javac
如上所述编译的合成方法.该实例被称为"功能对象"或"代理".
通过获取lambda metafactory来转储其代理类,您可以使用javap
反汇编字节码并将代理实例跟踪回生成它的lambda表达式.这可能是一个例子最好的例子.请考虑以下代码:
public class CaptureTest { static Listlist; 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正在捕捉?
首先要做的是编译这个类并运行javap -v -p CaptureTest
.该-v
选项显示反汇编的字节码和其他信息,如行号表.-p
必须提供该选项才能javap
反汇编私有方法.这个输出包括很多东西,但重要的部分是合成的lambda方法:
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中生成的每个方法.
然后,内存分析器下运行的程序,供给命令行参数-Djdk.internal.lambda.dumpProxyClasses=
的java
命令.这会导致lambda metafactory将其生成的类转储到指定的目录(必须已存在).
获取应用程序的内存配置文件并进行检查.有很多种方法可以做到这一点; 我使用了NetBeans内存分析器.当我运行它时,它告诉我一个带有10,000,000个元素的byte []由一个arg$1
名为的类中的字段保存CaptureTest$$Lambda$9
.这就是OP所得到的.
此类名称上的计数器没有用,因为它表示由lambda metafactory生成的类的序列号,按照它们在运行时生成的顺序.知道运行时序列并没有告诉我们它在源代码中的起源.
但是,我们已经要求lambda metafactory转储它的类,所以我们可以去看一下这个特定的类来看它的作用.实际上,在输出目录中,有一个文件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
.这是源文件中的lambda number 2(从零开始),它是bar()
方法中两个lambda表达式中的第一个.现在,您可以返回到javap
原始类的输出,并使用lambda静态方法中的行号信息来查找源文件中的位置.该CaptureTest.lambda$bar$2
方法的行号信息指向第29行.此位置的lambda是
() -> o.hashCode()
where o
是一个自由变量,它是bar()
方法的一个参数的捕获.