JAVA 프로그램은 여타 언어들처럼 프로그램 → 컴파일러(최적화) → 네이티브 코드이나, 프로그램 → 인터프리터(직접실행) 방식이 아닌 프로그램 → 컴파일러 → 가상머신(최적화 및 직접실행)의 방식으로 실행된다. 그 중에서 눈여겨 볼 수 있는 것은, 바로 실행시간 최적화를 하는 JIT 컴파일러인데, 오늘은 이에 대해 다루어본다.
개괄
JIT(Just-In-Time) 컴파일러는 런타임시 .class 바이트 코드를 네이티브 코드로 컴파일하여 JAVA 응용 프로그램의 성능을 향상시키는 런타임 환경의 구성 요소이다.
자바 프로그램은 여러 컴퓨터 아키텍처 환경에서 JVM이 해석할 수 있는 플랫폼 중립(platform-neutral)적인 바이트 코드를 포함하는 클래스로 구성된다. 런타임에 JVM은 클래스 파일을 로드하고 각 개별 바이트 코드를 해석하면서 적절한 계산을 수행한다. 해석 중에 추가 프로세서 및 메모리 사용을 하여 JAVA 응용 프로그램이 네이티브 프로그램보다 느리게 수행될 수 있다. JIT 컴파일러는 런타임에 바이트 코드를 네이티브 코드로 컴파일하여 JAVA 프로그램의 성능을 향상시킨다.
동작원리
JIT 컴파일러는 자바에 기본적으로 설정되어 있는 기능이다. 메서드가 컴파일되면 JVM은 해석하지 않고 직접 컴파일 메서드를 호출한다. 이론적으로 컴파일이 프로세서 시간과 메모리 사용을 필요로하지 않는다면 모든 메서드를 컴파일했을때 JAVA 프로그램의 속도가 네이티브 프로그램의 속도에 가까워 질 수 있다.
JVM이 처음 시작되면 수천 가지 메서드가 호출된다. 이렇게 모든 메서드를 컴파일하면 결국 프로그램이 매우 우수한 최대 성능을 달성하더라도 시작 시간에 상당한 영향을 줄 수 있다. 각 메서드에 대해 JVM은 사전 정의 된 컴파일 임계 값에서 시작하여 메서드가 호출 될 때마다 감소되는 호출 계수를 정의한다. 호출 계수가 0에 도달하면 메서드에 대한 just-in-time 컴파일이 시작된다. 따라서 자주 사용되는 메서드는 JVM이 시작된 직후에 컴파일되며 사용되지 않는 메서드는 훨씬 나중에 컴파일되거나 전혀 컴파일되지 않는다. JIT 컴파일 임계 값은 JVM이 빨리 시작하고 성능이 향상되도록 도와준다. 임계 값은 시작 시간과 장기간 성능 사이의 최적의 균형을 얻기 위해 선택되었다.
코드 최적화
컴파일을 위해 메서드를 선택하면 JVM은 해당 바이트 코드를 Just-In-Time 컴파일러 (JIT)에 공급한다. JIT는 메서드를 올바르게 컴파일하기 전에 바이트 코드의 구문을 해석한다. JIT 컴파일러가 메서드를 분석하는 것을 돕기 위해, 바이트 코드는 먼저 트리로 재작성된다. 이는 바이트 코드보다 기계 코드에 가깝다. 그 후 분석 및 최적화가 메서드의 트리에서 수행된다. 최종적으로 트리는 네이티브 코드로 변환된다.
컴파일은 다음 단계로 구성된다.
1. 인라인(Inlining)
인라인은 작은 메서드의 트리를 병합하거나 '인라인'하여 그들의 호출 트리를 만든다. 이렇게하면 자주 호출되는 메서드의 호출 속도가 빨라진다.
2. 지역 최적화(Local optimizations)
지역 최적화는 한 번에 코드의 작은 부분을 분석하고 성능을 향상시킨다. 주로 코드의 중복된 연산제거, 예측가능한 값의 대치 등이다.
3. 제어 흐름 최적화(Control flow optimizations)
제어 흐름 최적화는 메서드나 그 내부의 제어 흐름을 분석하고 코드 경로를 재정렬하여 효율성을 향상시킨다.
4. 전역 최적화(Global optimizations)
전역 최적화는 전체 메서드에서 즉시 작동한다. 그들은 더 비싼 컴파일 시간을 요구하지만 성능을 크게 향상시킬 수 있다.
5. 네이티브 코드 생성(Native code generation)
네이티브 코드 생성은 플랫폼 아키텍처에 따라 다르다. 일반적으로 컴파일러는 이 단계에서 메서드 트리를 네이티브 코드로 변환한다. 일부 작은 최적화가 아키텍처 특성에 따라 수행된다.
References