[Java] 람다 INVOKEDYNAMIC의 내부 동작에 대한 이해

반응형
728x90
반응형

익명클래스 vs 람다

익명클래스

public class AnonymousClass {
    public static void main(String[] args) {
        IntBinaryOperator plus = new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        };
    }
}

 

▷ 바이트코드

public class com/whiteship/white_ship_study/week15/AnonymousClass {
  static INNERCLASS com/whiteship/white_ship_study/week15/AnonymousClass$1 null null

  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 8 L0
    NEW com/whiteship/white_ship_study/week15/AnonymousClass$1
    DUP
    INVOKESPECIAL com/whiteship/white_ship_study/week15/AnonymousClass$1.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 14 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE plus Ljava/util/function/IntBinaryOperator; L1 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

1) 익명 클래스는 static 중첩 클래스로 새로운 AnonymousClass 를 생성하고 있다.

static INNERCLASS com/whiteship/white_ship_study/week15/AnonymousClass$1

 

2) INVOKESPECIAL 을 통해 AnonymousClass$1 의 클래스의 객체를 생성한다.

INVOKESPECIAL com/whiteship/white_ship_study/week15/AnonymousClass$1.<init> ()V

 

람다

public class LambdaEx {
    private IntBinaryOperator plus() {
        IntBinaryOperator plus = (x, y) -> {
            return x + y;
        };
        return plus;
    }
}

 

▷ 바이트코드

// class version 52.0 (52)
// access flags 0x21
public class com/whiteship/white_ship_study/week15/NotAnonymous {

  // compiled from: NotAnonymous.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x2
  private plus()Ljava/util/function/IntBinaryOperator;
   L0
    LINENUMBER 12 L0
    INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntBinaryOperator; [
      // handle kind 0x6 : 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;
      // arguments:
      (II)I, 
      // handle kind 0x6 : INVOKESTATIC
      com/whiteship/white_ship_study/week15/NotAnonymous.lambda$plus$0(II)I, 
      (II)I
    ]
    ASTORE 1
   L1
    LINENUMBER 15 L1
    ALOAD 1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week15/NotAnonymous; L0 L2 0
    LOCALVARIABLE plus Ljava/util/function/IntBinaryOperator; L1 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2

  // access flags 0x100A
  private static synthetic lambda$plus$0(II)I
    // parameter synthetic  x
    // parameter synthetic  y
   L0
    LINENUMBER 13 L0
    ILOAD 0
    ILOAD 1
    IADD
    IRETURN
   L1
    LOCALVARIABLE x I L0 L1 0
    LOCALVARIABLE y I L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

1) 위 익명클래스와 다르게, 클래스를 별도로 생성하는 코드는 없다. 대신, 메서드를 생성한다.

  • INVOKEDYNAMIC 키워드
  • LambdaMetafactory.metafactory()에 MethodHandles$Lookup, MethodType, MethodHandle 등을 인자를 넣어서 처리한다.
  • Callsite 객체 사용
INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntBinaryOperator; [
      // handle kind 0x6 : 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;
      // arguments:
      (II)I, 
      // handle kind 0x6 : INVOKESTATIC
      com/whiteship/white_ship_study/week15/NotAnonymous.lambda$plus$0(II)I, 
      (II)I
    ]

 

2) private static으로 plus 메서드가 자동 생성되었다.

private static synthetic lambda$plus$0(II)I

 

람다식을 이용한 구현 + 외부 참조

public class LambdaEx {
  private IntBinaryOperator plus() {
    IntBinaryOperator plus = (x, y) -> {
      System.out.println(this); // me.whiteship.chapter01.item03.LambdaEx1@1a407d53
      return x + y;
    };
    return plus;
  }
}

 

▷ 바이트코드

// class version 52.0 (52)
// access flags 0x21
public class com/whiteship/white_ship_study/week15/NotAnonymous {

  // compiled from: NotAnonymous.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x2
  private plus()Ljava/util/function/IntBinaryOperator;
   L0
    LINENUMBER 12 L0
    ALOAD 0
    INVOKEDYNAMIC applyAsInt(Lcom/whiteship/white_ship_study/week15/NotAnonymous;)Ljava/util/function/IntBinaryOperator; [
      // handle kind 0x6 : 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;
      // arguments:
      (II)I, 
      // handle kind 0x7 : INVOKESPECIAL
      com/whiteship/white_ship_study/week15/NotAnonymous.lambda$plus$0(II)I, 
      (II)I
    ]
    ASTORE 1
   L1
    LINENUMBER 16 L1
    ALOAD 1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week15/NotAnonymous; L0 L2 0
    LOCALVARIABLE plus Ljava/util/function/IntBinaryOperator; L1 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2

  // access flags 0x1002
  private synthetic lambda$plus$0(II)I
    // parameter synthetic  x
    // parameter synthetic  y
   L0
    LINENUMBER 13 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L1
    LINENUMBER 14 L1
    ILOAD 1
    ILOAD 2
    IADD
    IRETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week15/NotAnonymous; L0 L2 0
    LOCALVARIABLE x I L0 L2 1
    LOCALVARIABLE y I L0 L2 2
    MAXSTACK = 2
    MAXLOCALS = 3
}

1) this 객체를 사용함으로써 달라진 점이 있다.

  • 람다식은 새로운 메서드로 볼 수 있으며, 메서드에서 this 는 메서드 를 가진 클래스를 의미한다.
  • 따라서 람다식에서 this 는 람다를 포함하는 클래스의 인스턴스이다.
IntBinaryOperator plus = (x, y) -> {
    System.out.println(this); // me.whiteship.chapter01.item03.LambdaEx1@1a407d53
    return x + y;
};

 

2) this 를 사용하는 람다는 static 이 아닌 lambda&plus$0 을 생성한다.

  • this 라는 인스턴스는 static 이 아니기 때문이다.

 

this

현재 객체(instance)를 가리키는 참조 변수로, this는 인스턴스 메서드 내부에서만 사용 가능하다.

private plus()Ljava/util/function/IntBinaryOperator;

 

익명클래스 vs 람다식 의 결론

  • 익명 내부 클래스는 새로운 클래스를 생성하지만, 람다는 새로운 메서드를 생성하여 포함한다.
  • 익명 내부 클래스의 this는 새로 생성된 클래스다.
  • 람다의 this는 람다식을 포함하는 클래스(생성된 메서드가 존재하는 클래스)다.

 

 

바이트코드의 INVOKE 종류

  • INVOKEVIRTUAL : 객체의 인스턴스 메서드를 호출할 때 사용
  • INVOKESPECIAL : 객체의 생성자나 private 메서드, 그리고 상위 클래스의 메서드를 호출할 때 사용
  • INVOKESTATIC : 클래스 메서드를 호출할 때 사용
Java7
  • INVOKEINTERFACE : 인터페이스 메서드를 호출할 때 사용
  • INVOKEDYNAMIC : 런타임에 동적으로 바인딩되는 메서드를 호출할 때 사용

 

INVOKE opcode 뒷부분에는 다음과 같은 정보가 포함됩니다.

  • 호출할 메서드의 클래스 이름 : 메서드가 속한 클래스의 이름입니다.
  • 호출할 메서드의 이름 : 호출할 메서드의 이름입니다.
  • 호출할 메서드의 시그니처 : 호출할 메서드의 매개변수 타입과 반환 타입에 대한 정보입니다.

이 정보를 사용하여 JVM은 호출할 메서드를 식별하고, 실행할 때 적절한 매개변수를 전달하고, 메서드가 반환하는 값을 적절히 처리할 수 있다.

 

 

Opcode 예제

public class StringConcatenation {
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "world";
        StringBuilder sb = new StringBuilder();
        sb.append(str1).append(" ").append(str2);
        String result = sb.toString();
        System.out.println(result);
    }
}

 

▷ 바이트코드

   0: ldc           #2    // String Hello
   2: astore_1
   3: ldc           #3    // String world
   5: astore_2
   6: new           #4    // class java/lang/StringBuilder
   9: dup
  10: invokespecial #5    // Method java/lang/StringBuilder."<init>":()V
  13: aload_1
  14: invokevirtual #6    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  17: ldc           #7    // String
  19: invokevirtual #6    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  22: aload_2
  23: invokevirtual #6    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  26: invokevirtual #8    // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  29: astore_3
  30: getstatic     #9    // Field java/lang/System.out:Ljava/io/PrintStream;
  33: aload_3
  34: invokevirtual #10   // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  37: return

1) StringBuilder 객체 sb의 append() 메서드를 호출하기 위한 코드

  14: invokevirtual #6    // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

INVOKEVIRTUAL opcode 뒷부분에는 호출할 메서드의 클래스 이름(java/lang/StringBuilder), 호출할 메서드의 이름(append), 호출할 메서드의 시그니처(매개변수 타입이 String이고 반환 타입이 StringBuilder인 메서드)에 대한 정보가 포함된것을 볼 수 있다.

 

 

JAVA 7 의 INVOKEDYNAMIC의 등장

컴파일 타임이 아닌 런타임에 어떤 메서드를 실행할지 결정하기 위해 등장했다.

INVOKEDYNAMIC 호출을 위해서는 세가지 정보가 필요하다.

  • Bootstrap method : invokedynamic 호출이 처음으로 발생할 때 호출되는 메서드로, 호출할 메서드를 결정하기 위해 호출
  • 정적 파라미터 목록 : 호출될 메서드의 매개변수 중 상수값이나 상수식으로 정의된 인자들
  • 동적 파라미터 목록 : 호출될 메서드의 매개변수 중 런타임 시점에 동적으로 계산되는 인자들

 

▷ 람다의 바이트코드

public class SimpleLambda {  
    public static void main(String[] args) {
        Runnable lambda= invokedynamic(
            bootstrap=LambdaMetafactory,
            staticargs=[Runnable, lambda$0],
            dynargs=[]);
        lambda.run();
    }
    private static void lambda$0() {
        System.out.println(1);
    }
}

1) bootstrap = LambdaMetafactory
LambdaMetafactory 의 metafactory() 를 자동으로 호출하는데, 이것이 bootstrap 메서드이다.

public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
    AbstractValidatingLambdaMetafactory mf;
    mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                         invokedName, samMethodType,
                                         implMethod, instantiatedMethodType,
                                         false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
    mf.validateMetafactoryArgs();
    return mf.buildCallSite();
}

mf.buildCallSite()가 호출되면 내부에서 함수형 인터페이스를 구현하는 클래스가 동적으로 만들어지며, 최종적으로 타겟 메서드의 핸들이 들어간 CallSite를 반환하게된다.

 


2) 여러가지 메서드 타입에 대한 정보를 가지고 CallSite 객체를 받는다.

CallSite 객체란?

동적으로 생성된 람다 표현식의 호출 지점을 나타내는 객체

abstract
public class CallSite {

    // The actual payload of this call site:
    /*package-private*/
    MethodHandle target;    // Note: This field is known to the JVM.  Do not change.
    
    //...
}

 

CallSite 객체는 런타임에 람다 표현식을 호출하기 위한 메서드 핸들(MethodHandle)을 담고 있으며, 이 메서드 핸들은 람다 표현식의 구현을 나타내는 메서드에 연결된다. 
런타임에 CallSite 객체가 람다 표현식의 호출을 수행할 때, 이 메서드 핸들을 사용하여 람다 표현식을 실행한다.

 

메서드 핸들(MethodHandle)을 가져오는 방법

룩업 컨텍스트(lookup context)을 통해서 가져와야한다. 컨텍스트를 가져오는 일반적인 방법은 정적 헬퍼 메서드인 MethodHandles.lookup()을 호출하는 것이다.

 

메서드 핸들을 이용해서 정적 메서드인 String.format(String, Object...)를 호출하는 예제

public class InvokeDynamicExample {
    public static void main(String [] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        // String.format(String format, Object... args)
        MethodType type = MethodType.methodType(String.class, String.class, Object[].class);
        MethodHandle mh = lookup.findStatic(String.class, "format", type);

        // String.format("Hello, %s!", "World");
        String s = (String) mh.invokeExact("Hello, %s!", new Object[]{"World"});
        System.out.println(s); // Hello, World!
    }
}

invokeExact()로 넘겨준 타입과 클래스, 메서드명과 정확히 일치하는 정적 메서드를 호출한다.

 

staticargs=[Runnable, lambda$0]

LambdaMetafactory가 metafactory 메서드를 통해 CallSite 객체를 생성할 때, 람다 표현식이 구현해야 할 인터페이스를 Runnable로, 람다 표현식의 구현을 담당하는 메서드의 참조를 lambda$0로 지정한다는 것을 나타낸다.


CallSite 객체는 이후에 런타임 시점에 람다 표현식을 호출할 때 사용되며, 람다 표현식의 구현을 담당하는 메서드 lambda$0는 MethodHandle로 변환되어 CallSite 객체에 저장된다.

4) staticargs=[Runnable, lambda$0]
정적 파라미터 목록이다.

  • Runnable : 람다 표현식이 구현할 인터페이스 유형
  • lambda$0 : 람다 표현식의 구현을 담당하는 메서드의 참조
    • lambda$0 메서드는 Runnable 인터페이스의 run 메서드와 시그니처가 동일하다.
    • 위에서 받은 CallStie 객체는 'lambda' 변수에 할당되는 것이다. (그래서 람다 표현식의 호출 지점이라고 한다.)

 

5) dynargs=[]
동적 파라미터 목록으로, 아까 봤던 람다 범위 이외의 다른 참조들을 포함한다. (this 등)

 

 

 

INVOKEDYNAMIC 정리

 

1) bootstrap 메서드 호출

 

2) CallSite 객체 반환받는다.

 

 

3) CallSite 객체로 어떤 메서드를 실행할지 결정한다.

 

 

 

References.

https://blog.hexabrain.net/400

https://tourspace.tistory.com/11

https://alkhwa-113.tistory.com/entry/%EB%9E%8C%EB%8B%A4%EC%8B%9Dfeat-%EC%9D%B5%EB%AA%85-%EA%B5%AC%ED%98%84-%ED%81%B4%EB%9E%98%EC%8A%A4-vs-%EB%9E%8C%EB%8B%A4%EC%8B%9D

 

반응형

'Coding > Java' 카테고리의 다른 글

[Java] CompletableFuture 클래스 (Future, CompletionStage)  (0) 2023.12.19
플라이웨이트 패턴 (Flyweight Pattern)  (0) 2023.04.02
wait()과 notify(), notifyAll()  (0) 2022.08.17
[JAVA] ThreadLocal  (0) 2022.08.04
[JAVA] Volatile 변수  (0) 2022.08.03

Designed by JB FACTORY