[Анализ лямбда-выражений в Heap-дампе

Очень интересно наблюдать, как многому мы можем научиться, обмениваясь знаниями!

Примером тому могут служить комментарии, которые я получил к последней опубликованной мною статье — «Выражения 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 — Джошуа Блох;

  1. Javadoc: java.lang.invoke.LambdaMetafactory

  2. Проблема: JDK-8023524 — Механизм дампа сгенерированных лямбда-классов / журнал генерации лямбда-кода

  3. StackOverflow: Поиск лямбды Java по ее искаженному имени в дампе кучи

  4. Eclipse Memory Analyzer(MAT)

  5. JBang;

Оцените статью
devanswers.ru
Добавить комментарий