Гипотеза ОП верна, что arg$1
— это поле лямбда-объекта, содержащее захваченное значение. Ответ от lukeg находится на правильном пути, заставляя метафабрику лямбда сбрасывать свои прокси-классы. (+1)
Вот подход, который использует инструмент javap
для отслеживания экземпляра, содержащего ссылку на исходный код. По сути, вы найдете правильный класс прокси; разобрать его, чтобы узнать, какой синтетический лямбда-метод он вызывает; затем свяжите этот синтетический лямбда-метод с конкретным лямбда-выражением в исходном коде.
(Большинство, если не вся эта информация относится к Oracle JDK и OpenJDK. Она может не работать для других реализаций JDK. Кроме того, это может быть изменено в будущем. Это должно работать с любой последней версией Oracle JDK 8 или OpenJDK 8, хотя. Вероятно, он продолжит работать в JDK 9.)
Сначала немного предыстории. Когда исходный файл, содержащий лямбда-выражения, скомпилирован, javac
скомпилирует тела лямбда-выражений в синтетические методы, которые находятся в содержащем классе. Эти методы являются приватными и статическими, и их имена будут примерно такими, как lambda$<method>$<count>
, где метод — это имя метода, содержащего лямбду, а count — это последовательный счетчик, который нумерует методов с начала исходного файла (начиная с нуля).
Когда лямбда-выражение впервые вычисляется во время выполнения, вызывается метафабрика лямбда. Это создает класс, реализующий функциональный интерфейс лямбды. Он создает экземпляр этого класса, принимает аргументы метода функционального интерфейса (если есть), объединяет их с любыми захваченными значениями и вызывает синтетический метод, скомпилированный javac
, как описано выше. Этот экземпляр называется «функциональным объектом» или «прокси».
Заставив лямбда-метафабрику выгружать свои прокси-классы, вы можете использовать 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
}
}
Этот код выделяет большой массив, а затем вычисляет четыре разных лямбда-выражения. Один из них фиксирует ссылку на большой массив. (Вы можете определить это путем осмотра, если знаете, что ищете, но иногда это сложно.) Какая лямбда выполняет захват?
Первое, что нужно сделать, это скомпилировать этот класс и запустить 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
Счетчик в конце имен методов начинается с нуля и нумеруется последовательно с начала файла. Кроме того, имя синтетического метода включает имя метода, содержащего лямбда-выражение, поэтому мы можем сказать, какой метод был сгенерирован из каждой из нескольких лямбда-выражений, встречающихся в одном методе.
Затем запустите программу под профилировщиком памяти, указав аргумент командной строки -Djdk.internal.lambda.dumpProxyClasses=<outputdir>
для команды java
. Это приводит к тому, что метафабрика лямбда выгружает свои сгенерированные классы в именованный каталог (который уже должен существовать).
Получите профиль памяти приложения и проверьте его. Есть множество способов сделать это; Я использовал профилировщик памяти NetBeans. Когда я запустил его, он сказал мне, что byte[] с 10 000 000 элементов содержится в поле arg$1
в классе с именем CaptureTest$$Lambda$9
. Это то, что получил ОП.
Счетчик для этого имени класса бесполезен, так как он представляет собой порядковый номер классов, созданных метафабрикой лямбда, в том порядке, в котором они были созданы во время выполнения. Знание последовательности времени выполнения не очень много говорит нам о том, где она возникла в исходном коде.
Однако мы попросили лямбда-метафабрику выгрузить свои классы, чтобы мы могли посмотреть на этот конкретный класс и посмотреть, что он делает. Действительно, в выходном каталоге есть файл 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
. Это лямбда номер 2 (начиная с нуля) в нашем исходном файле, и это первое из двух лямбда-выражений в методе bar()
. Теперь вы можете вернуться к выходным данным javap
исходного класса и использовать информацию о номере строки из статического метода лямбда, чтобы найти местоположение в исходном файле. Информация о номере строки метода CaptureTest.lambda$bar$2
указывает на строку 29. Лямбда в этом месте
() -> o.hashCode()
где o
— свободная переменная, которая является захватом одного из аргументов метода bar()
.
person
Stuart Marks
schedule
11.01.2017