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

Я ищу утечку памяти, и дамп кучи показывает мне, что несколько экземпляров лямбда содержат объекты-нарушители. Имя лямбды — это имя окружающего класса с $$lambda$107 в конце. Я также вижу, что у него есть одно поле (это правильное имя для него), называемое arg$1, которое ссылается на объекты, заполняющие кучу. К сожалению, у меня довольно много лямбда-выражений в этом классе, и мне интересно, что я могу сделать, чтобы сузить его.

Я предполагаю, что arg$1 — это неявный аргумент — свободная переменная в лямбда-выражении, которая захватывается, когда лямбда становится замыканием. Это правильно?

Я также предполагаю, что 107 не является реальной помощью в изоляции, но есть ли какие-то флаги, которые я могу установить, чтобы регистрировать, какое лямбда-выражение получает какой номер?

Какие еще полезные советы?


person ExMathGuy    schedule 10.01.2017    source источник


Ответы (3)


Гипотеза ОП верна, что 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
comment
Это улов. Как вы сказали, числа в именах лямбда-классов являются порядковыми номерами порядка генерации во время выполнения. Таким образом, их сопоставление с реальными лямбда-выражениями зависит от точного порядка вычисления лямбда-выражений, который может различаться в разных запусках сложных программ. - person Holger; 20.01.2017
comment
@ Хольгер Верно. Экземпляр, захвативший большой объект, может быть не CaptureTest$$Lambda$9; например, это может быть CaptureTest$$Lambda$347. Дамп кучи говорит вам, какой это класс. Когда у вас есть этот класс, вы можете разобрать его, чтобы узнать, какой статический метод он вызывает. Этот статический метод можно проследить до исходного кода. Правила именования статических методов лямбда-выражения не определены, но они стабильны и достаточно предсказуемы. - person Stuart Marks; 21.01.2017
comment
@Holger Таблица номеров строк из разборки статического метода также указывает на нужное место в исходном файле. Я отредактировал ответ, чтобы включить это. - person Stuart Marks; 21.01.2017
comment
«Если у вас есть этот класс, вы можете его дизассемблировать», предполагая, что у вас уже есть дамп классов из того же прогона, где имена совпадают. Если у вас есть существующий дамп кучи, это может быть не так просто. Но это может помочь отсортировать все лямбда-классы, которые никогда не могут ссылаться на рассматриваемый объект из-за типа их полей. Ну или поля должны совпадать с параметрами целевого метода, т.е. в вашем примере я вижу, что есть только один синтетический метод с параметром ссылочного типа, следовательно, я уже знаю, какое лямбда-выражение должно отвечать, даже без свалка кучи. - person Holger; 23.01.2017

Это немного запутанно, но вы можете попробовать:

  • запуск вашей JVM с -Djdk.internal.lambda.dumpProxyClasses=/path/to/directory/. Опция заставит JVM выгружать сгенерированные прокси-объекты (файлы классов) в каталог по вашему выбору.

  • вы можете попробовать декомпилировать сгенерированные таким образом классы. Я создал пример кода Java, в котором использовались лямбда-выражения, а затем открыл один из сгенерированных файлов классов (файл с именем Test$$Lambda$3.class) в Intellij Idea, и он был декомпилирован в:

    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);
        }
    }
    
  • оттуда вы можете вывести тип лямбды (IntPredicate в примере), имя класса, в котором он был определен (Test), и имя метода, в котором он был определен (bar).

person lukeg    schedule 10.01.2017

Число довольно бесполезно, так как оно определяется во время выполнения в том порядке, в котором каждая лямбда в указанном классе была создана (встречена) - если это правильно, у вас есть более 100 лямбда в этом одном классе. См. Что означает $$ в имени, сгенерированном javac?

Если вы не можете ограничить исследование проблемных лямбд на основе того, на что они ссылаются, то лучше всего немного упростить класс или преобразовать некоторые из наиболее явных подозреваемых в ссылки на методы.

person john16384    schedule 10.01.2017