Be-Developer

JVM 밑바닥까지 파헤치기 : 8 바이트코드 실행 엔진

8 바이트 코드 실행 엔진

  • VM에서 실행엔진이 바이트코드를 실행하는 방법
    1. 해석실행 : 인터프리터를 통한 실행
    2. 컴파일실행 : JIT컴파일러로 네이티브 코드 생성 후 실행.

2. 런타임 스택 프레임 구조

  • 스택프레임
    • 지역 변수 테이블
    • 피연산자 스택
    • 동적 링크
    • 메서드 반환주소 ,,
  • 스택프레임에 할당해야하는 메모리 크기(피연산자 스택에 필요한 깊이, 지역변수테이블의 크기)는 런타임에는 영향받지않고, 프로그램 소스 코드와 특정 가상머신 구현의 스택 메모리 레이아웃에 달려있다.

2.1. 지역변수 테이블

  • 메서드 매개변수와 메서드 안에서 정의된 지역변수 저장하는 공간.
  • 변수슬롯 하나의 크기는 32bit or 64bit이고, 인덱스방식으로 저장된다.(N번째 변수 슬롯)
  • boolean,byte,char,short,int,float
  • 참조타입 : 객체의 자바 힙 내에서 시작주소 혹은 인덱스, 타입정보를 직간접적으로 알 수 있다.
  • returnAddress : 다른 바이트코드 명령어의 주소를 알려주는 용도, jump용도로 사용된다.
  • 메서드 호출시 매개변수들도 지역변수테이블을 통해 전달된다. 0번째 : this, 1..n 매개변수들, n..m : 메서드 지역변수들
  • pc값이 변수의 유효범위를 벗어나면, 해당 변수를 담고있던 슬롯은 재활용될 수 있다.
      void main(){
        {
          byte[] placeholder = new byte[64*1024*1024];  // 64MB
        }
        int a = 0;  // checkpoint
        System.gc();
      }
    
    • int 선언이 없으면 gc시에 메모리 회수가 되지 않음. (변수 슬롯이 사용중)
    • int 선언이 있으면 gc시에 메모리 회수됨. (변수 슬롯이 재활용됨)
  • 메모리 release목적으로 변수사용 종료 후 null을 선언하는 경우
    • 장 : 스택프레임을 장기간 재활용할 수 없으며, JIT 컴파일을 촉발할 만큼 메서드가 자주 호출되지 않을때 효과적.
    • 단 : JIT 컴파일러가 Null할당을 잘못된 작업으로 판단하여 무시할 가능성이 높다. 없던 코드가 될 수 있음.
    • 대안 : 변수 범위를 적절히 지정하여 변수가 회수되는 시간을 제어하는것이 바람직.
  • 지역변수에는 클래스 로딩단계중 ‘준비’단계가 없어 초기값이 세팅되지않는다.
    class A {
    private int classField;   // 초기값 0으로 자동 할당.
    public void test(){
      int localField;   // 컴파일러에서 오류 혹은 바이트코드 검증단계에서 오류.
    }
    }
    

2.2 피연산자 스택

  • 피연산자 스택 -> 다른 스택 프레임 정보 -> 지역변수테이블
  • 하부 스택 프레임의 피연산자 스택 일부가 상부 스택 프레임의 지역변수 테이블과 겹쳐지는 경우, 최적화 과정에서 스택프레임을 공유하기도 한다.

2.3 동적 링크

  • 정적 해석 : 메서드 스택프레임에 있는 심벌참조중 일부는 클래스 로딩단계에서 참조가 처음 사용될때 직접참조로 바뀐다.
  • 동적 링크 : 그 외 실행중에 직접 참조로 변환되는것.

2.4 반환 주소

  • 메서드를 종료하는 방식
    1. 반환 바이트코드
    2. 예외

3. 메서드 호출

  • 어떤 메서드 버전을 사용할지 정하는 것. (상속관계에서 메서드 버전선택)

3.1 해석

  • 정적 해석 : 컴파일러가 프로그램 코드를 컴파일하는 시점에 호출대상이 정해지는것
    • invokestatic, invokespecial 로 호출, 비가상 메서드 호출가능.
    • 비가상 메서드 : 정적 메서드와 private 메서드, 인스턴스 생성자, 부모 클래스 메서드, final 메서드
    • 클래스가 로드될때 심벌참조를 직접참조로 변환한다.

3.2 디스패치

정적 디스패치 : 메서드 버전 선택에 정적타입을 참고하는 모든 디스패치작업.

  • VM은 컴파일시에 매개변수의 정적타입을 참고하여 컴파일시점에 어떤 오버로딩 버전이 호출될지를 정한다.
  • ex) 메서드 오버로딩 (매개변수만 다른거) ```java class Caller { void sayHello(Human human); void sayHello(Man human); void sayHello(Women human); }

void main(){ Human humanMan = new Man(); Man man = new Man(); // Human : 정적타입, 겉보기타입 // Man : 실제타입, 런타임 타입

// java code                        // 컴파일 후 class file에서 메서드 심벌참조
caller.sayHello(humanMan)           // Caller.sayHello(Human)
caller.sayHello((Man)humanMan)     // Caller.sayHello(Man)
caller.sayHello(man)              // Caller.sayHello(man)   }   ```

동적 디스패치

  • 런타임에 실제 타입을 보고 메서드 버전을 결정하는 방식
  • ex) 메서드 오버라이딩
    void main(){
      Human humanMan = new Man();
      Human womanMan = new Woman();
    
      // java code                        // 컴파일 후 class file에서 메서드 심벌참조   // 런타임
      humanMan.sayHello();                // Human.sayHello()                   // Man.sayHello()
      womanMan.sayHello();                // Human.sayHello()     .             // Woman.sayHello
      }
    
  • 가상 필드?
    • 상속관계에서 하위클래스 필드는 상위클래스 필드를 가리며, 다형성과는 무관.
    • 필드 초기화 순서는 super() 가 먼저 호출되므로, 부모필드값 먼저 할당받음.

단일/다중 디스패치

  • 메서드의 수신객체와 매개변수를 합쳐서 메서드 볼륨이라고 한다.
  • 단일 디스패치 : 한개의 볼륨안에서 대상 메서드 선택
  • 다중 디스패치 : 둘이상
    class Human {
      void call(Man human)
      void call(Woman woman)
    }
    class Man extends Human {
    
    }
    
    void main(){
      Man man = new Man();
      Woman woman = new Woman();
      Human womanMan = new Woman();
    
      // java code                        // 컴파일 후 class file에서 메서드 심벌참조
      man.call(woman)                     // Man.call(Woman)
      man.call(womanMan)                  // Man.call(Human)    //이러면 뭐가 호출되는거지?
    }
    
    • 컴파일 단계 (정적 디스패치) : call 메서드 2개중 매개변수타입으로 선택 = 다중 디스패치
    • 런타임 단계 (동적 디스패치) : 수신객체 (man)의 실제타입 선택 = 단일 디스패치
  • java 언더는 정적 다중 디스패치 + 동적 단일 디스패치이다.
  • java var 는 컴파일타임에 타입추론 = 정적 디스패치.
디스패치 종류 메서드 결정 시점 기준 특징
정적 단일 디스패치 컴파일 타임 리시버 객체의 타입 - 메서드 호출이 컴파일 시점에 결정됨
- 메서드 오버로딩에서 사용
- 성능이 좋고 런타임 오버헤드 없음
동적 단일 디스패치 런타임 리시버 객체의 실제 타입 - 메서드 호출이 런타임 시점에 객체의 실제 타입에 따라 결정됨
- 메서드 오버라이딩에서 사용
- 유연하지만 런타임 오버헤드 발생
정적 다중 디스패치 컴파일 타임 모든 인자의 타입 - 모든 인자의 타입을 고려하여 호출할 메서드를 컴파일 시점에 결정
- 자바는 직접 지원하지 않음
- 설계가 복잡할 수 있음
동적 다중 디스패치 런타임 모든 인자의 실제 타입 - 모든 인자의 실제 타입을 고려하여 호출할 메서드를 런타임에 결정
- 더블 디스패치

4. 동적 타입언어 지원

  • 동적 언어 타입 : 타입검사 과정중 주요 단계들이 런타임에 수행된다.
    • ex) closure, elrang, groovy, js, php, python,,
    • 변수자체에는 타입이 없고, 변수의 값에만 타입이 있다.
    • 장점 : 자율성, 타입명시 코드가 없어지므로 코드가 명확하고 간결해짐. 개발 효율,생산성 개선.
  • 정적 언어타입 : 타입검사를 컴파일타임에 수행.
    • ex) java, c++
    • 장점 : 컴파일러가 타입검사를 엄격하게 진행하여 코드 작성과정에서 버그 발견 가능. 안정성이 뛰어남.
  • 예시
      int[][][] array = new int[1][0][-1]
    
    • 컴파일은 잘되지만 NegativeArraySizeException런타임예외 발생.
  • 런타임 예외 : 해당 코드를 실행하지 않는 한 문제가 없다.
  • 링크타임 예외 : 실행되지않는 경로에 있더라도, 클래스가 로딩될때 발생.

4.2 자바와 동적 타이핑

  • ~JDK6 : 동적타입 언어를 구현하려면, 런타임에 수많은 클래스를 로드하면서 메모리부하가 늘어났고, 인라인최적화를 하지못함.
  • JDK7~ : JVM레벨에서 동적타입언어를 지원

4.3 java.lang.invoke package

  • MethodHandler 객체는 최종 호출 메서드를 가리키는 ‘참조’로 간주될 수 있다.
  • 리플렉션과의 차이
    • reflection
      • 자바 코드수준에서 시뮬레이션
      • 메서드 시그니처, 서술자, 속성테이블 등 많은 정보포함
      • 오직 자바언어를 위해 설계됨
    • MethodHandler
      • 바이트코드 수준에서 시뮬레이션
      • 메서드 실행과 관련한 정보만 담긴다.
      • JVM에서 동작하는 모든 언어 지원

4.4 invokedynamic

  • 자바 탄생 이후 유일하게 추가된 바이트코드 명령어
  • MethodHandle은 상위수준 코드와 API로 구현되고, invokedynamic 은 바이트코드와 클래스의 속성 및 상수 수준에서 구현된다.
  • JDK8~ interface default method 가 도입되면서 자바도 invokedynamic 이점을 누리기 시작

5. 스택기반 바이트코드 해석 및 실행 엔진

  • 해석 실행 : 인터프리터가 실행하는 것
  • 컴파일 실행 : JIT 컴파일러를 써서 네이티브 코드로 변환해 실행하는것.