8 바이트 코드 실행 엔진
- VM에서 실행엔진이 바이트코드를 실행하는 방법
- 해석실행 : 인터프리터를 통한 실행
- 컴파일실행 : 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 반환 주소
- 메서드를 종료하는 방식
- 반환 바이트코드
- 예외
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에서 동작하는 모든 언어 지원
- reflection
4.4 invokedynamic
- 자바 탄생 이후 유일하게 추가된 바이트코드 명령어
- MethodHandle은 상위수준 코드와 API로 구현되고, invokedynamic 은 바이트코드와 클래스의 속성 및 상수 수준에서 구현된다.
- JDK8~ interface default method 가 도입되면서 자바도 invokedynamic 이점을 누리기 시작
5. 스택기반 바이트코드 해석 및 실행 엔진
- 해석 실행 : 인터프리터가 실행하는 것
- 컴파일 실행 : JIT 컴파일러를 써서 네이티브 코드로 변환해 실행하는것.