2015-04-16 8 views
18

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

public static Object o = new Object(); 

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

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

Метод x2() всегда будет возвращать один и тот же объект Ламба, в то время как 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 решает, где их использовать или нет?

+0

Не ответ на ваш вопрос, но я предполагаю, что каждый раз, когда вы вызываете 'x1()', он создает новый объект 'x', что, очевидно, делает лямбду другой. Вопреки этому, 'x2' всегда совпадает с возвратом статического объекта, который не изменяется. – Crembo

+0

@Crembo интересно догадаться, но если вы измените его на '() -> новый Object()', он также вернет тот же лямбда-объект. – Andremoniy

+0

@Crembo BTW, в 'x1()' нет новых объектов – Andremoniy

ответ

8

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

Это указано в JLS §15.27.4:

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

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

+0

Вы имеете в виду, что 'x2()' в моем примере * может * возвращать разные объекты? – Andremoniy

+1

@Andremoniy Да, он может. Вот что говорят спекуляции. –

+0

С полным уважением к вам, я не вижу в этой части директивы 'JLS' того факта, что создать новый экземпляр или использовать существующий, - это неопределенный behaivour. Он просто говорит, что он может быть использован повторно или может быть создан новым. – Andremoniy

3

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

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

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

Desugaring пример - «без гражданства» лямбды

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

... и ...

Desugaring пример - лямбды захвата неизменные значения

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

Ваш второй метод (х2) является примером первого типа Ламба (лица без одного, который не захватывает не состояние из это ограждающих сферы), и это, вероятно, почему же 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

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

Декомпиляция ваши 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 

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

#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; 

Как пояснялось here:

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

Запись кэша Constant Pool («СРС»), когда проблема будет решено, имеет один или два слово метаданных и/или информации о смещении.

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

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

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

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

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

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

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

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

+0

Не могли бы вы объяснить это более подробно по моей глупости: я не могу понять вашу мысль о 'invokedynamic' – Andremoniy

+1

Отредактированный, довольно сложный вопрос. –

+0

спасибо, и согласны ли вы с тем, что мой вопрос дублируется? Если нет, я призываю вас повторно открыть его. – Andremoniy

5

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

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

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

Для примера, то desuggared версия будет выглядеть следующим кодом отрезала с 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 инструкцию обязана вызывать фабричный метод, который определяется первым классом на сайте вызова. Для второго класса фабрика не создается, так как класс полностью не имеет гражданства. Поэтому метод bootstrap создает экземпляр singleton класса и привязывает экземпляр непосредственно к сайту вызова (с использованием константы MethodHandle).

Чтобы вызвать статические методы из другого класса, для загрузки классов лямбда используется загрузчик анонимного класса. Если вы хотите узнать больше, я недавно summarized my findings on lambda expressions.

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

+1

'Lambda $ x2' не имеет' Object o', не так ли? В этом и состоит этот вопрос. – zeroflagL

+1

Jap, скопируйте ошибку, скорректированную сейчас. –

0

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

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

+0

Лямбда, возвращаемая 'x1', не использует' o'. И 'o' не должен был быть« окончательным », так как это не переменная. – zeroflagL

+0

@zeroflagL 'x1()' зависит от 'o' - если внешний вызывающий объект получает объект функции из' x1', а затем устанавливает 'o' для обращения к другому объекту, возвращаемая функция все равно должна предоставлять объект, который был в 'o', когда вызывается' x1', поэтому было бы неверно, чтобы 'x1' всегда возвращал тот же Callable. – yjo

+0

Возвращенная функция ссылается только на 'x'. Даже если 'o' был окончательным, экземпляры лямбда все равно будут отличаться для каждого вызова. 'o' не имеет значения. – zeroflagL