Очень интересно наблюдать, как многому мы можем научиться, обмениваясь знаниями!
Примером тому могут служить комментарии, которые я получил к последней опубликованной мною статье — «Выражения Lambdas не являются анонимными классами»!
Спасибо вам всем!
Один из этих комментариев был очень интересным, практически мини-статья, где Веллингтон Домициано, мотивированный темой, исследовал и поделился своими выводами при изучении лямбда-выражений и того, как спецификация преобразует эти лямбда-выражения в реализации функциональных интерфейсов.
Мы с Жуаном Виктором Мартинсом даже согласились и порекомендовали Веллингтону переписать эту мини-статью в полноценную. Это взаимодействие было очень хорошим, и я надеюсь, что оно будет продолжаться всегда, потому что все мы, сообщество, выиграем от этого!
Что ж, интересная вещь о выражениях Lambdas — понять, как они реализованы.
В предыдущей статье мы использовали утилиту javap для визуализации класса (*.class), сгенерированного из заданного источника, с выражениями lambdas и проверки того, что lambdas вызываются операторами InvokeDynamic.
Помните ли вы мини-статью Веллингтона?
Well, Wellington, с его очень большим вкладом через его комментарий, продемонстрировал очень интересное использование для визуализации внутренних классов, полученных из ламбдас, и это пробудило во мне больше любопытства, и я хотел бы предложить кое-что очень интересное! Спасибо Веллингтон!
Выгрузка внутренних классов, производных от лямбд
Основываясь на мини-статье Веллингтона, давайте составим следующий класс:
import java.util.List;
import java.util.function.Consumer;
public class CreatingLambdaExpression {
public static void main(String... args) {
Consumer<String> printer = (text) -> System.out.println(text);
List.of("Wellington", "João Vitor", "Maximillian")
.forEach(printer);
}
}
Этот источник станет *.class ниже:
bin/ $ tree.
.
└── CreatingLambdaExpression.class
А если мы выполним приведенную ниже команду, то сможем немного подробнее изучить, как был составлен сценарий с лямбдой:
bin/ $ javap -c -p CreatingLambdaExpression
Compiled from "CreatingLambdaExpression.java"
public class CreatingLambdaExpression {
public CreatingLambdaExpression();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String...);
Code:
0: invokedynamic #16, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
5: astore_1
6: ldc #20 // String Wellington
8: ldc #22 // String João Vitor
10: ldc #24 // String Maximillian
12: invokestatic #26 // InterfaceMethod java/util/List.of:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List;
15: aload_1
16: invokeinterface #32, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
21: return
private static void lambda$0(java.lang.String);
Code:
0: getstatic #44 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #50 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
}
Но мы можем пойти дальше, благодаря этому ответу, которым поделился Wellington, просто добавьте следующее свойство в выполнение программы:
-Djdk.internal.lambda.dumpProxyClasses=<dump directory>
P.S: Когда вы используете это свойство, я рекомендую указывать пустой каталог, потому что JVM будет загружать не только внутренние классы, сгенерированные вашим проектом, но и внутренние классы из сторонних библиотек, которые использует ваш проект. Это загрязнит ваш каталог и, возможно, это не то, чего вы хотите, верно?
Давайте используем его при выполнении нашего кода:
bin/ $ java -Djdk.internal.lambda.dumpProxyClasses=. CreatingLambdaExpression
Wellington
João Vitor
Maximillian
Хорошо, код выполнился, как и ожидалось, но теперь мы можем увидеть внутренний класс, производный от лямбды, в нашем исходнике, загруженном в директории bin:
bin/ $ tree .
.
├── CreatingLambdaExpression$$Lambda$1.class
└── CreatingLambdaExpression.class
А затем мы можем просмотреть его класс с помощью утилиты javap
bin/ $ javap -c -p CreatingLambdaExpression$$Lambda$1.class
final class CreatingLambdaExpression$$Lambda$1 implements java.util.function.Consumer {
private CreatingLambdaExpression$$Lambda$1();
Code:
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
public void accept(java.lang.Object);
Code:
0: aload_1
1: checkcast #14 // class java/lang/String
4: invokestatic #20 // Method CreatingLambdaExpression.lambda$main$0:(Ljava/lang/String;)V
7: return
}
Из этого можно сделать вывод, что:
Когда JVM сталкивается с лямбдой, она использует инструкции ASM, содержащиеся в самой JVM, для построения необходимых внутренних классов.
Производные классы лямбда генерируются «на лету» компонентом LambdaMetafactory1компонента, то есть во время выполнения JRE.
Приятно знать, что эти вещи были разработаны, но также приятно знать, почему они были разработаны!
Открытие «ПОЧЕМУ
Исследуя немного больше об этом свойстве, я пришел к следующему вопросу JDK-80235242:
Что ж, причина здесь предельно ясна:
«Метафабрика лямбд генерирует классы на лету. По соображениям поддержки и удобства обслуживания желательно иметь возможность инспектировать эти классы…».
Dando uma traduzida para o português:
«Метафабрика лямбд генерирует классы во время выполнения. Для поддержки и удобства обслуживания желательно иметь возможность проверять эти классы…».
Да, поддержка и обслуживание!
Чтобы изучить и помочь пониманию, давайте рассмотрим сценарий: найти лямбду, которая вызывает возможную утечку памяти через heap-dump!
Поиск ламбд из дампа кучи
Вдохновлен проблемой на StackOverflow: Поиск лямбды Java по ее искаженному имени в дампе кучи3Мы собираемся реализовать нашу программу, которая создаст сценарий, близкий к утечке памяти, но давайте попробуем воспользоваться инструментами и понять, почему было важно создать это свойство jdk.internal.lambda.dumpProxyClasses:
import java.util.Arrays;
import java.util.List;
import java.util.function.IntSupplier;
import java.util.stream.Collectors;
public class CollectingIntegerListFromLambdaExpressions {
static List<IntSupplier> list;
static IntSupplier suppliersA(Object o) {
return () -> 0;
}
static IntSupplier suppliersB(Object o) {
int h = o.hashCode();
return () -> h;
}
static IntSupplier suppliersC(Object o) {
return () -> o.hashCode();
}
static IntSupplier suppliersD(Object o) {
int len = o.toString().length();
return () -> len;
}
static void run() {
Object big = new byte[10_000_000];
list = Arrays.asList(
suppliersA(big),
suppliersB(big),
suppliersC(big),
suppliersD(big));
System.out.println("It's done! A list of integers has been created!");
System.out.println("the generated list of integers is : %s"
.formatted(list.stream().map(IntSupplier::getAsInt).collect(Collectors.toList())));
}
public static void main(String... args) throws InterruptedException {
run();
System.out.println("""
Keeping the application alive in order to let aheap dump be taken.
Press CTRL+C to close the application.
""");
Thread.sleep(Long.MAX_VALUE);
}
}
TL;DR
В принципе, в нашей программе мы создадим список объектов функций типа java.util.function.IntSupplier, которые будут предоставляться методами через выражения lambdas, и из этого списка мы сгенерируем список целых чисел и запишем его содержимое в вывод программы, однако, чтобы иметь возможность выполнить дамп кучи, главный поток будет спать через метод Thread.sleep;
Проблема заключается в методе suppliersC, и этот метод вернет лямбду, которая будет хранить ссылку на аргумент o, имеющий тип Object, который на самом деле получит экземпляр байтового массива с 10.000.000 позициями во время выполнения.
Эти экземпляры привязаны к главному потоку, и поскольку он спит, сборщик мусора не сможет удалить эти экземпляры из памяти, что вызовет сценарий утечки памяти.
конец TL;DR
В результате компиляции исходного текста получился вот такой *.class:
bin/ $ tree .
.
└── CollectingIntegerListFromLambdaExpressions.class
Затем запустите приложение:
bin/ $ java CollectingIntegerListFromLambdaExpressions
It's done! A list of integers has been created!
the generated list of integers is : [0, 2124308362, 2124308362, 11]
Keeping the application alive in order to let aheap dump be taken.
Press CTRL+C to close the application.
А для захвата дампа кучи я использовал Eclipse Memory Analyzer(MAT)4:
По данным анализатора, экземпляр типа класса CollectingIntegerListFromLambdaExpressions$$Lambda$3 потребляет примерно 10 000 016 байт (90,83%) памяти кучи.
Но этот класс был создан «на лету» из некоторой лямбды, которую мы никак не можем проанализировать, поскольку у нас нет к ней доступа!
Хорошо, мы знаем, что проблема в лямбде, которая возвращается из метода suppliersC(Object o), но в реальной жизни у нас будет гораздо больше классов и методов, возможно, это заставит, по крайней мере, приложить усилия и потратить больше времени, чтобы найти эту точку, читая источники. Вот где это свойство может нам помочь!
Так что давайте активируем его и посмотрим, как мы можем быть более настойчивыми, чтобы найти эту проклятую лямбду!
bin/ $ java -Djdk.internal.lambda.dumpProxyClasses=. CollectingIntegerListFromLambdaExpressions
It's done! A list of integers has been created!
the generated list of integers is : [0, 1627674070, 1627674070, 11]
Keeping the application alive in order to let aheap dump be taken.
Press CTRL+C to close the application.
Хорошо, теперь мы можем увидеть внутренние классы, полученные из нашего источника:
bin/ $ tree .
.
├── CollectingIntegerListFromLambdaExpressions$$Lambda$1.class
├── CollectingIntegerListFromLambdaExpressions$$Lambda$2.class
├── CollectingIntegerListFromLambdaExpressions$$Lambda$3.class
├── CollectingIntegerListFromLambdaExpressions$$Lambda$4.class
├── CollectingIntegerListFromLambdaExpressions$$Lambda$5.class
├── CollectingIntegerListFromLambdaExpressions.class
└── java
└── util
└── stream
├── Collectors$$Lambda$6.class
├── Collectors$$Lambda$7.class
└── Collectors$$Lambda$8.class
3 directories, 9 files
Как мы видим, все внутренние классы, полученные из нашего источника, были загружены в каталог bin.
P.S: При использовании этого свойства я рекомендую указывать пустой каталог, потому что JVM будет загружать не только внутренние классы, сгенерированные вашим проектом, но и внутренние классы из сторонних библиотек, которые использует ваш проект. Это загрязнит ваш каталог и, возможно, это не то, чего вы хотите, верно?
Теперь давайте просмотрим CollectingIntegerListFromLambdaExpressions$$Lambda$3.class:
bin/ $ javap -c -p CollectingIntegerListFromLambdaExpressions$$Lambda$3
final class CollectingIntegerListFromLambdaExpressions$$Lambda$3 implements java.util.function.IntSupplier {
private final java.lang.Object arg$1;
private CollectingIntegerListFromLambdaExpressions$$Lambda$3(java.lang.Object);
Code:
0: aload_0
1: invokespecial #13 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #15 // Field arg$1:Ljava/lang/Object;
9: return
public int getAsInt();
Code:
0: aload_0
1: getfield #15 // Field arg$1:Ljava/lang/Object;
4: invokestatic #23 // Method CollectingIntegerListFromLambdaExpressions.lambda$suppliersC$2:(Ljava/lang/Object;)I
7: ireturn
}
Мы видим, что этот класс получает в конструкторе аргумент типа java.lang.Object, а внутри метода getAsInt() мы видим оператор invokestatic, который с комментарием указывает на вызов статического метода CollectingIntegerListFromLambdaExpressions.lambda$suppliersC$2.
Здесь мы уже можем увидеть имя метода, который инстанцировал объект, вызывающий утечку памяти.
Но давайте предварительно просмотрим класс CollectingIntegerListFromLambdaExpressions.class и увидим немного больше деталей:
bin/ $ javap -c -p CollectingIntegerListFromLambdaExpressions
Compiled from "CollectingIntegerListFromLambdaExpressions.java"
public class CollectingIntegerListFromLambdaExpressions {
static java.util.List<java.util.function.IntSupplier> list;
public CollectingIntegerListFromLambdaExpressions();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static java.util.function.IntSupplier suppliersA(java.lang.Object);
Code:
0: invokedynamic #7, 0 // InvokeDynamic #0:getAsInt:()Ljava/util/function/IntSupplier;
5: areturn
static java.util.function.IntSupplier suppliersB(java.lang.Object);
Code:
0: aload_0
1: invokevirtual #11 // Method java/lang/Object.hashCode:()I
4: istore_1
5: iload_1
6: invokedynamic #15, 0 // InvokeDynamic #1:getAsInt:(I)Ljava/util/function/IntSupplier;
11: areturn
static java.util.function.IntSupplier suppliersC(java.lang.Object);
Code:
0: aload_0
1: invokedynamic #18, 0 // InvokeDynamic #2:getAsInt:(Ljava/lang/Object;)Ljava/util/function/IntSupplier;
6: areturn
static java.util.function.IntSupplier suppliersD(java.lang.Object);
Code:
0: aload_0
1: invokevirtual #21 // Method java/lang/Object.toString:()Ljava/lang/String;
4: invokevirtual #25 // Method java/lang/String.length:()I
7: istore_1
8: iload_1
9: invokedynamic #15, 0 // InvokeDynamic #1:getAsInt:(I)Ljava/util/function/IntSupplier;
14: areturn
static void run();
Code:
0: ldc #30 // int 10000000
2: newarray byte
4: astore_0
5: iconst_4
6: anewarray #31 // class java/util/function/IntSupplier
9: dup
10: iconst_0
11: aload_0
12: invokestatic #33 // Method suppliersA:(Ljava/lang/Object;)Ljava/util/function/IntSupplier;
15: aastore
16: dup
17: iconst_1
18: aload_0
19: invokestatic #38 // Method suppliersB:(Ljava/lang/Object;)Ljava/util/function/IntSupplier;
22: aastore
23: dup
24: iconst_2
25: aload_0
26: invokestatic #41 // Method suppliersC:(Ljava/lang/Object;)Ljava/util/function/IntSupplier;
29: aastore
30: dup
31: iconst_3
32: aload_0
33: invokestatic #44 // Method suppliersD:(Ljava/lang/Object;)Ljava/util/function/IntSupplier;
36: aastore
37: invokestatic #47 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
40: putstatic #53 // Field list:Ljava/util/List;
43: getstatic #57 // Field java/lang/System.out:Ljava/io/PrintStream;
46: ldc #63 // String It's done! A list of integers has been created!
48: invokevirtual #65 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
51: getstatic #57 // Field java/lang/System.out:Ljava/io/PrintStream;
54: ldc #71 // String the generated list of integers is : %s
56: iconst_1
57: anewarray #2 // class java/lang/Object
60: dup
61: iconst_0
62: getstatic #53 // Field list:Ljava/util/List;
65: invokeinterface #73, 1 // InterfaceMethod java/util/List.stream:()Ljava/util/stream/Stream;
70: invokedynamic #79, 0 // InvokeDynamic #3:apply:()Ljava/util/function/Function;
75: invokeinterface #83, 2 // InterfaceMethod java/util/stream/Stream.map:(Ljava/util/function/Function;)Ljava/util/stream/Stream;
80: invokestatic #89 // Method java/util/stream/Collectors.toList:()Ljava/util/stream/Collector;
83: invokeinterface #95, 2 // InterfaceMethod java/util/stream/Stream.collect:(Ljava/util/stream/Collector;)Ljava/lang/Object;
88: aastore
89: invokevirtual #99 // Method java/lang/String.formatted:([Ljava/lang/Object;)Ljava/lang/String;
92: invokevirtual #65 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
95: return
public static void main(java.lang.String...) throws java.lang.InterruptedException;
Code:
0: invokestatic #103 // Method run:()V
3: getstatic #57 // Field java/lang/System.out:Ljava/io/PrintStream;
6: ldc #106 // String Keeping the application alive in order to let aheap dump be taken.nPress CTRL+C to close the application.n
8: invokevirtual #65 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: ldc2_w #110 // long 9223372036854775807l
14: invokestatic #112 // Method java/lang/Thread.sleep:(J)V
17: return
private static int lambda$suppliersC$2(java.lang.Object);
Code:
0: aload_0
1: invokevirtual #11 // Method java/lang/Object.hashCode:()I
4: ireturn
private static int lambda$suppliersB$1(int);
Code:
0: iload_0
1: ireturn
private static int lambda$suppliersA$0();
Code:
0: iconst_0
1: ireturn
}
Итак, мы видим, насколько это свойство может помочь нам, если мы столкнемся с подобными проблемами, в которых нам необходимо проанализировать внутренние классы, сгенерированные из выражений lambdas.
И снова я хотел бы поблагодарить Веллингтона за то, что он поделился своим опытом, и я надеюсь, что я мог бы поделиться со всеми вами своим опытом, и что это подстегнет желание всех вас делиться и помогать друг другу все больше и больше, как и должно быть в сообществе!
Спасибо всем и до следующей статьи!!!
Источник примеров: 5
- CreatingLambdaExpression.java
- CollectingIntegerListFromLambdaExpressions.java
Ссылки
- «Функциональное программирование на Java: использование возможностей лямбда-выражений Java 8» Венкат Субраманиам
- : Книга: Эффективная Java — Джошуа Блох;
-
Javadoc: java.lang.invoke.LambdaMetafactory
-
Проблема: JDK-8023524 — Механизм дампа сгенерированных лямбда-классов / журнал генерации лямбда-кода
-
StackOverflow: Поиск лямбды Java по ее искаженному имени в дампе кучи
-
Eclipse Memory Analyzer(MAT)
-
JBang;