Подтвердить что ты не робот

Когда JVM решает повторно использовать старую лямбду?

Рассмотрим следующий фрагмент кода:

public static Object o = new Object();

public static Callable x1() {
    Object x = o;
    return () -> x;
}

public static Callable x2() {
    return () -> o;
}

Метод x2() всегда будет возвращать один и тот же объект lamba, а x1() всегда будет создавать новый:

    System.out.println(x1());
    System.out.println(x1());
    System.out.println(x2());
    System.out.println(x2());

распечатает что-то вроде этого:

TestLambda$$Lambda$1/[email protected]
TestLambda$$Lambda$1/[email protected]
TestLambda$$Lambda$2/[email protected]
TestLambda$$Lambda$2/[email protected]

Где (в спецификации JVM я предполагаю?) это правило повторного использования лямбда описано? Как JVM решает, где их использовать или нет?

4b9b3361

Ответ 1

Вы не можете быть уверены в идентичности возвращаемого объекта для выражения лямбда. Это может быть новый экземпляр или ранее существовавший экземпляр.

Это указано в JLS & sect; 15.27.4:

Во время выполнения оценка выражения лямбда аналогична оценке выражения создания экземпляра класса, поскольку нормальное завершение создает ссылку на объект. Оценка лямбда-выражения отличается от выполнения лямбда-тела.

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

Ответ 2

После некоторых исследований похоже, что это зависит от того, что создание лямбда-выражений выполняется через invokedynamic и что вы см. побочный эффект того, как invokedynamic ведет себя в JVM Oracle.

Декомпиляция методов x1() и x2():

public static java.util.concurrent.Callable x1();
Code:
  stack=1, locals=1, args_size=0
     0: getstatic     #2                  // Field o:Ljava/lang/Object;
     3: astore_0
     4: aload_0
     5: invokedynamic #3,  0              // InvokeDynamic #0:call:(Ljava/lang/Object;)Ljava/util/concurrent/Callable;
    10: areturn

public static java.util.concurrent.Callable x2();
Code:
  stack=1, locals=0, args_size=0
     0: invokedynamic #4,  0              // InvokeDynamic #1:call:()Ljava/util/concurrent/Callable;
     5: areturn

Соответствующий раздел пула констант:

 #3 = InvokeDynamic      #0:#37         // #0:call:(Ljava/lang/Object;)Ljava/util/concurrent/Callable;
 #4 = InvokeDynamic      #1:#39         // #1:call:()Ljava/util/concurrent/Callable;

BootstrapMethods:

0: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
  #35 ()Ljava/lang/Object;
  #36 invokestatic Test.lambda$x1$0:(Ljava/lang/Object;)Ljava/lang/Object;
  #35 ()Ljava/lang/Object;
1: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
  #35 ()Ljava/lang/Object;
  #38 invokestatic Test.lambda$x2$1:()Ljava/lang/Object;
  #35 ()Ljava/lang/Object;

Как объяснено здесь:

Поскольку каждая ссылка invokedynamic указывает (в общем) на другой сайт вызова (у нас есть два сайта вызова, по одному для каждой функции xN), кеш константного пула должен содержать отдельный запись для каждой invokedynamic инструкции. (Другие инструкции для вызова могут совместно использовать записи кэша CP, если они используют одну и ту же символическую ссылку в постоянный пул.)

Запись кэша константного пула ( "CPCE" ) при разрешении имеет одно или два слова из метаданных и/или информации о смещении.

Для invokedynamic, разрешенный CPCE содержит указатель метода * конкретный метод адаптера, обеспечивающий точное поведение вызова. Существует также ссылочный параметр, связанный с сайтом вызова называемое приложением, которое хранится в массиве resol_references для CPCE.

Метод называется адаптером, потому что (вообще говоря) он перетасовывает аргументы, извлекает дескриптор целевого метода из вызова сайта и вызывает дескриптор метода.

Дополнительный ссылочный параметр называется приложением, поскольку он добавляется в список аргументов, когда invokedynamic инструкция выполняется.

Обычно приложение является ссылкой CallSite, созданной bootstrap, но JVM не заботится об этом. Пока адаптера в CPCE знает, что делать с сохраненным приложением с CPCE, все хорошо.

В качестве углового случая, если значение приложения равно null, оно не нажимается на all, а метод адаптера не должен ожидать дополнительного аргумента. The адаптер в этом случае может быть постоянной ссылкой на статический метод с сигнатурой, совместимой с invokedynamic инструкция. Фактически это превратит invokedynamic в простой invokestatic. Многие другие такие оптимизации снижения прочности возможно.

Я интерпретирую, что "Это по сути будет поворот", поскольку это означает, что в таких обстоятельствах (адаптер без параметров) invokedynamic будет эффективно вести себя так же, как и invokestatic, и что адаптер будет кэшироваться и повторно использоваться.

Все это характерно для JVM Oracle, но я подозреваю, что в отношении этого аспекта это один из самых очевидных вариантов, и я ожидаю увидеть что-то подобное даже в других реализациях jvm.

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

Ответ 3

Как уже отмечалось , фактическое поведение не указано JLS, будущей версии разрешено выводить из текущей реализации пока JLS остается заполненным.

Вот что происходит в текущей версии HotSpot:

Любое лямбда-выражение связывается с помощью сайта invokedynamic call. Этот сайт звонков запрашивает метод начальной загрузки для привязки factory к экземпляру, который реализует функциональный интерфейс выражения лямбда. В качестве аргументов любые переменные, необходимые для выполнения выражения лямбда, передаются в factory. Тело выражения лямбда вместо этого копируется в метод внутри класса.

В вашем примере версия с отладкой будет выглядеть следующим образом: с помощью команды invokedynamic в угловых скобках:

class Foo {
  public static Object o = new Object();

  public static Callable x1() {
    Object x = o;
    return Bootstrap.<makeCallable>(x);
  }

  private static Object lambda$x1(Object x) { return x; }

  public static Callable x2() {
    return Bootstrap.<makeCallable>();
  }

  private static void lambda$x2() { return Foo.o; }
}

Метод boostrap (который фактически находится в java.lang.invoke.LambdaMetafactory), затем предлагается связать сайт вызова с его первым вызовом. Для лямбда-выражений эта привязка никогда не изменится, поэтому метод начальной загрузки вызывается только один раз. Чтобы связать класс, реализующий функциональный интерфейс, метод начальной загрузки сначала должен создать класс во время выполнения, который выглядит следующим образом:

class Lambda$x1 implements Callable {
  private static Callable make(Object x) { return new Lambda$x1(x); }
  private final Object x; // constructor omitted
  @Override public Object call() { return x; }
}

class Lambda$x2 implements Callable {
  @Override public Object call() { return Foo.o; } 
}

После создания этих классов команда invokedynamic обязана вызывать метод factory, который определяется первым классом на сайте вызова. Для второго класса не создается factory, так как класс полностью без гражданства. Поэтому метод bootstrap создает экземпляр singleton класса и привязывает экземпляр непосредственно к сайту вызова (используя константу MethodHandle).

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

Но опять же всегда ссылается на спецификацию, а не на реализацию. Это может измениться!

Ответ 4

(Отредактировано это, поскольку мой предыдущий ответ был мусором!)

В этом документе http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html содержится объяснение.

Эти разделы из этого документа должны помочь ответить на ваш вопрос...

Пример обесцвечивания - "безгражданство" лямбда

Простейшая форма выражения лямбда для перевода - это та, которая не фиксирует состояние из его охватывающей области (безстоящей лямбда):

... и...

Пример desugaring - lambdas фиксирует неизменные значения

Другая форма лямбда-выражения включает в себя захват заключений конечных (или фактически окончательных) локальных переменных и/или полей из окружающих экземпляров (которые мы можем рассматривать как захват финала, охватывающего эту ссылку).

Второй метод (x2) - это пример первого типа lamba (безгражданный, который не фиксирует никакого состояния из него, охватывающего область), и это, вероятно, почему в каждом случае возвращается одна и та же lamba.

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

>javap -p -c L2.class

public class L2 {
  public static java.lang.Object o;

  public L2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static java.util.concurrent.Callable<java.lang.Object> x1();
    Code:
       0: getstatic     #2                  // Field o:Ljava/lang/Object;
       3: astore_0
       4: aload_0
       5: invokedynamic #3,  0              // InvokeDynamic #0:call:(Ljava/lang/Object;)Ljava/util/concurrent/Callable;
      10: areturn

  public static java.util.concurrent.Callable<java.lang.Object> x2();
    Code:
       0: invokedynamic #4,  0              // InvokeDynamic #1:call:()Ljava/util/concurrent/Callable;
       5: areturn

  private static java.lang.Object lambda$x2$1() throws java.lang.Exception;
    Code:
       0: getstatic     #2                  // Field o:Ljava/lang/Object;
       3: areturn

  private static java.lang.Object lambda$x1$0(java.lang.Object) throws java.lang.Exception;
    Code:
       0: aload_0
       1: areturn

  static {};
    Code:
       0: new           #5                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: putstatic     #2                  // Field o:Ljava/lang/Object;
      10: return
}

Ответ 5

У компилятора нет возможности оптимизировать x1() для возврата той же лямбда - поведение тогда будет иным. Поскольку o не является окончательным, возвращаемое лямбда должно захватывать состояние этого поля (с переменной x), поскольку его значение может меняться между вызовом x1() и вызовом возвращенной лямбда.

Чтобы не сказать, что не существует ситуаций, когда компилятор теоретически может повторно использовать экземпляр, но не делает этого (другие ответы дают некоторое представление об этом) - только это не один из этих случаев.