본문으로 바로가기

람다의 내부동작 #2

category 개발이야기/Java 2017. 9. 26. 23:09
반응형

지난글에서 람다의 내부/외부 표현에 대한 글을 작성했습니다.

이번에는 람다의 내부적인 동작에 대한 상세한 이해를 위해 JVM 관련 부분과 bytecode 위주로 언급합니다.


JVM의 opcode

compile된 bytecode를 보면 네종류의 opcode를 사용하여 Java의 method를 표현합니다.

    • 1.7 이전

      • invokestatic: static method 실행

      • invokevirtual: instance method 실행

      • invokeinterface: interface method 실행

      • invokespecial: exact한 함수 수행 - override 불가, 더이상 변화가 없는 함수들

        • 생성자, private method, super call

    • invokedynamic

      • 동적 타입 언어를 위한 opcode

      • Jruby, Jython, Groovy 같은 JVM에서 돌아가는 동적 타입언어를 지원하기 위해 추가.

      • Java8 부터 default method, lambda compile시에 사용


1.7 이전에는 네종류의 opcode를사용했으나, java8 부터는 람다의 경우 invokedynamic이라는 opcode를 사용합니다. invokedynamic은 1.7에서 새롭게 추가되었지만, 1.7에서 javac로 compile된 bytecode에는 절대 사용 되지 않습니다.


원래의 용도가 java가 아닌 다른 동적타입 언어였기 때문이죠.

invokedynamic에 대한 코드를 보기전에 기존 코드들이 bytecode상 어떻게 변경되는지 보겠습니다.


또한 각각의 opcode의 실행 순서는 거의 유사합니다.

다만 invokedynamic만 완전히 다른 형태로 실행 됩니다.

Translate Strategy

Java8의 람다는 bytecode로 변경하는 방법과 동작방식이기존의 방법과는 좀 다릅니다.
람다를 bytecode로 표현하는 방식들은 기존에도, 그리고 다른 언어에도 아래와 같이 여러 방법으로 표현되었습니다.
  • inner class
  • method handles
  • dynamic proxies
  • others..
이러한 방법으로 람다를 표현함에 있어 두가지 상반되는 목표를 갖게 되는데, 첫번째로 "특정 전략을 따르지 않고, 미래의 최적화를 위해 유연성을 극대화 하는 방법""클래스 파일 표현에 안정성을 제공하는 방법" 입니다.

예를 들어 inner class로 람다를 치환하여 표현할 경우 전자를 단점으로 남지만 후자는 장점으로 같습니다.

Java8에서는 두가지 목표를 전부 달성하기 위하여 아래와 같은 전략을 취합니다.
  1. compile시에 bytecode에서는 람다를 구현하는 객체를 생성하지 않음 -> runtime에 실제 생성을 위함하는 방법만(recipe)만 표기
  2. 해당 recipe는 invokedynamic instruction에 동적/정적 인수 목록으로 encoding 된다.

참고: http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html

Invokedynamic call

invokedynamic의 동작은 아래와 같은 동작으로 나뉩니다.
  • indy가 호출되면 bootstrap영역의 lambdafactory.metafactory()를 수행
    • lambdafactory.metafactory(): java runtime library의 표준화 method
    • 어떤 방법으로 객체를 생성할지 dynamically 결정
      • 클래스 생성, 재사용, proxy, wrapper class, VM전용 API사용등등 성능 향상을 위한 최적화된 방법 사용
  • java.lang.invoke.CallSite 객체를 return함.
    • 해당 lambda의 lambda factory
    • MethodHandle을 멤버변수로 가짐
    • 람다가 변환되는 함수 인터페이스의 인스턴스를 반환
    • 한번만 생성되고 재 호출시 재 사용함. 
      • (In a number of the possible translation strategies, we need to generate new classes. For example, if we are generating a class-per-lambda (spinning inner classes at runtime instead of compile time), we generate the class the first time a given lambda factory site is called. Thereafter, future calls to that lambda factory site will re-use the class generated on the first call.)


CallSite는 내부에 java.lang.invoke.MethodHandle을 멤버 변수로 갖고, MethodHanlde은 람다와 연결된 private method로 연결됩니다.

Lambda Desugaring

Lambda는 private static method로 풀어집니다.

또한 이런 부분은 bytecode에서도 확인할 수 있습니다.


그럼 마지막으로 #1에서 나왔던 예제의 bytecode를 다시 확인해 보겠습니다.

Runnable lambda = () -> check(this)
lambda.run();


실제 performance는 단순 inner class를 치환하는것보다 훨씬 빠르다는 것을 테스트를 통해 증명한 내용이 아래 링크에 있습니다.

동영상을 다 보기 힘드신분은 Performance example - capture cost 부분만 돌려서 보시면 됩니다.^^

참고:https://www.infoq.com/presentations/lambda-invokedynamic

반응형

'개발이야기 > Java' 카테고리의 다른 글

Java의 메모리 관리 - Weak, Soft, Phantom reference 예제  (0) 2017.10.29
Weak reference의 이해  (1) 2017.10.22
람다의 내부동작 #1  (0) 2017.09.24
Java 8 Comparator  (0) 2017.09.14
Java 8 String join  (2) 2017.09.13