클래스 로더를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이트코드는 실행 엔진에 의해 실행된다. 실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다. CPU가 기계 명령어을 하나씩 실행하는 것과 비슷하다. 바이트코드의 각 명령어는 1바이트짜리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음, 다음 OpCode를 수행하는 식으로 동작한다.
그런데 자바 바이트코드는 기계가 바로 수행할 수 있는 언어보다는 비교적 인간이 보기 편한 형태로 기술된 것이다. 그래서 실행 엔진은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하며, 그 방식은 다음 두 가지가 있다.
바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나 하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기하는 인터프리터 언어의 단점을 그대로 가지는 것이다. 즉, 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.
JVM 인터프리터는 런타임(runtime) 중에 바이트 코드를 한 라인씩 읽고 실행합니다. 여기에서 실행 시간이 느리다는 문제가 발생합니다. 바이트 코드 역시 기계어로 변환되어야 하기 때문에 C, C++ 처럼 미리 컴파일을 통해 기계어로 변경되는 언어에 비해 속도가 느려집니다. 따라서 이를 개선해주는 것이 JIT 컴파일러입니다.
인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시점(런타임 중)에 바이트코드를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.
코드 컴파일을 수행할 기준을 의미합니다. 컴파일 임계치를 만족하는 코드는 JIT 컴파일러에 의해 컴파일이 수행됩니다.
method entry counter(JVM 내에 있는 메서드가 호출된 횟수)
back-edge loop counter(메서드가 루프를 빠져나오기까지 회전한 횟수)
두 카운터의 합계를 확인하고 메서드의 바이트 코드를 컴파일 할 지 결정합니다.
back-edge loop counter 값을 위한 임계치 = Compile threshold * OnStackReplacePercentage / 100
클라이언트 컴파일은 1500, 서버 컴파일은 10000 값이 컴파일 임계치 디폴트 값입니다.
VM 옵션 지정 방법
-XX:CompileThreshold=N
-XX:OnStackReplacePercentage=N
$ java -XX:CompileThreshold=100 -XX:OnStackReplacePercentage=33 src/test/java/JitCompilerTest.java
컴파일이 완료된 코드로 변경하는 작업을 의미합니다. 대상 코드가 컴파일이 완료된 상태가 되었음에도 최적화되지 않은 코드가 수행되고 있는 것이 발견되는 경우 이를 수행합니다. 혹은 인터프리터에 의해 수행되는 중에 오랫동안 루프가 지속되는 경우 사용됩니다. 루프가 끝나지 않고 지속적으로 수행되고 있는 경우에 큰 도움을 줄 수 있습니다.
다음과 같은 코드가 있을 때, 실행시간을 보면 86번 째와 308번째에서 실행시간이 급격하게 줄어든 것을 확인할 수있다.
public class JitCompilerTest {
public static void main(String[] args) {
int a = 0;
for (int index = 0; index < 500; index++) {
long startTime = System.nanoTime();
for (int subIndex = 0; subIndex < 1000; subIndex++) {
a++;
}
System.out.println("loop count: " + index + ", execution time: " + (System.nanoTime() - startTime));
}
}
}
실행엔진이 동작하는지는 JVM명세에 규정되지 않았다. 따라서 JVM벤더들은 다양한 기법으로 실행엔진을 향상시키고 다양한 방식의 JIT 컴파일러를 도입하고 있다.
대부분의 JIT 컴파일러는 다음과 같은 형태로 동작한다.
JIT 컴파일러는 바이트코드를 일단 중간 단계의 표현인 IR(Intermediate Representation)로 변환하여 최적화를 수행하고 그 다음에 네이티브 코드를 생성한다.
오라클 핫스팟 VM은 핫스팟 컴파일러라고 불리는 JIT 컴파일러를 사용한다. 핫스팟이라 불리는 이유는 내부적으로 프로파일링을 통해 가장 컴파일이 필요한 부분, 즉 '핫스팟' 을 찾아낸 다음, 이 핫스팟을 네이티브 코드로 컴파일하기 때문이다. 핫스팟 VM은 한번 컴파일된 바이트코드라도 해당 메서드가 더 이상 자주 불리지 않는다면, 즉 핫스팟이 아니게 된다면 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터 모드로 동작한다.
핫스팟 VM은 서버 VM과 클라이언트 VM으로 나뉘어 있고, 각각 다른 JIT 컴파일러를 사용한다. 클라이언트 VM과 서버 VM은 동일한 런타임을 사용하지만, 그림과 같이 다른 JIT 컴파일러를 사용한다
※프로파일링 (위키피디아)
프로파일링(profiling, 프로그램 프로파일링/소프트웨어 프로파일링) 또는 성능 분석은 프로그램의 시간 복잡도 및 공간(메모리), 특정 명령어 이용, 함수 호출의 주기와 빈도 등을 측정하는 동적 프로그램 분석의 한 형태이다. 프로파일링 정보는 대개가 프로그램 최적화를 보조하기 위해 사용된다. 프로파일링은 프로파일러(profiler)라는 도구를 사용하여 프로그램 소스 코드나 이진 실행 파일을 계측 분석함으로써 수행한다.
Client compiler called C1. -It compiles quick but less optimized and used for quick startup such as GUI applications.
Server compiler called C2. It takes slower compilation time, but more optimized code. It’s predominantly used for long running Server-Side applications.
※클라이언트 컴파일러는 서버 컴파일러보다 상대적으로 빠르게 더 많은 코드를 컴파일하기 때문에 프로그램이 실행되면 클라이언트 컴파일러가 더 빠를 것이다. 하지만 상대적으로 늦게 컴파일하는 서버 컴파일러는 그 만큼 코드를 검사하는 시간을 갖고 더욱 최적화된 코드 컴파일을 할 수 있게 된다.
클라이언트와 서버 두 컴파일러의 장점을 조합한 컴파일러이다.
먼저 클라이언트로 스타트업 시간을 빠르게 하고, 많이 쓰이는 부분은 서버 컴파일러로 재컴파일하는 방식을 사용한다.
출처
JVM D2
5장.Execution Enging -김한도
자바 JIT 컴파일러 - 자바 성능 분석 - 자세한 성능 튜닝관련은 다음 참고