2017-01-10 3 views
7

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

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

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

Любые другие полезные советы?

ответ

5

гипотеза ОП является правильной, что arg$1 является полем объекта, содержащего лямбда захваченного значения. Ответ от lukeg находится на правильном пути, в процессе получения метаданных лямбды, чтобы сбросить свои классы прокси. (+1)

Вот такой подход, который использует инструмент javap для отслеживания экземпляра, содержащего ссылку на исходный код. В основном вы найдете правильный класс прокси; разобрать его, чтобы узнать, какой синтетический метод лямбда он называет; затем свяжите этот синтетический лямбда-метод с конкретным лямбда-выражением в исходном коде.

(Большая часть, если не вся эта информация относится к Oracle JDK и OpenJDK. Возможно, это не сработает для разных реализаций JDK, и это может измениться в будущем. Это должно работать с любым недавним Oracle JDK 8 или OpenJDK 8. Вероятно, он продолжит работу в JDK 9.)

Во-первых, немного фона.Когда скомпилирован исходный файл, содержащий lambdas, 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. Это приводит к тому, что lambda metafactory выгружает созданные классы в указанный каталог (который уже должен существовать).

Получить профиль памяти приложения и проверить его. Существует множество способов сделать это; Я использовал профилировщик памяти NetBeans. Когда я запустил его, он сказал мне, что байт [] с 10 000 000 элементов удерживался полем arg$1 в классе с именем CaptureTest$$Lambda$9. Это до тех пор, пока OP не получил.

Счетчик этого имени класса не является полезным, поскольку он представляет собой порядковое число классов, сгенерированных лямбда-метафайлой, в том порядке, в котором они были сгенерированы во время выполнения. Знание последовательности выполнения не говорит нам очень много о том, где она возникла в исходном коде.

Однако мы попросили лямбда-метафайлу сбросить свои классы, чтобы мы могли посмотреть этот конкретный класс, чтобы посмотреть, что он делает. Действительно, в выходном каталоге есть файл 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().

+0

Это улов. Как вы сказали, номера в именах лямбда-классов являются порядковыми номерами порядка генерации времени выполнения. Поэтому их сопоставление с реальными лямбда-выражениями зависит от точного порядка оценки лямбда-выражений, которые могут отличаться в разных прогонах для сложных программ. – Holger

+0

@ Holger Right. Экземпляр, который захватил большой объект, может быть не 'CaptureTest $$ Lambda $ 9'; это может быть 'CaptureTest $$ Lambda $ 347', например. Дамп кучи сообщает вам, какой класс он есть. Когда у вас есть этот класс, вы можете разобрать его, чтобы узнать, какой статический метод он вызывает. Этот статический метод можно проследить до исходного кода. Правила для обозначения статических методов лямбда не указаны, но они стабильны и довольно предсказуемы. –

+0

@Holger Таблица номеров строк от разборки статического метода также указывает на нужное место в исходном файле. Я отредактировал ответ, чтобы включить это. –

0

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

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

3

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

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