JIT 컴파일(just-in-time compilation) 또는 동적 번역(dynamic translation)은 프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일 기법이다.
컴파일러 vs 인터프리터
컴파일러와 인터프리터 모두 high-level language를 machine language로 번역한다.
컴파일러는 소스 코드(high-level language로 작성) 전체를 링커 등을 통해 한번에 번역하여 목적 파일(기계어로 작성)로 만들어 메모리상에 적재한다.
인터프리터는 소스 코드를 한 행씩 중간 코드로 번역 후 실행한다.
- 컴파일러는 소스코드 전체를 컴퓨터 프로세서가 실행할 수 있도록 바로 기계어로 변환한다. 인터프리터는 고레벨 언어를 중간 코드(intermediate code)로 변환하고 이를 각 행마다 실행한다.
- 일반적으로 컴파일러가 각 행마다 실행하는 특성을 가진 인터프리터보다는 실행시간이 빠르다.
- 컴파일러는 전체 소스코드를 변환 한 뒤 에러를 보고하지만 인터프리터는 각 행마다 실행하는 도중 에러가 보고되면 이후 작성된 코드를 살펴보지 않는다. 이는 보안적인 관점에서 도움이 된다.
- 예를 들어 파이썬은 인터프리트 언어이고 C, C++는 컴파일 언어이다. 자바는 컴파일러와 인터프리터 모두 사용한다.
컴파일러
- 장점
- 컴파일이 완료된 실행 파일은 컴퓨터에서 빠르게 실행할 수 있기 때문에 효율적이다.
- 0과 1로 된 기계어로 번역되기 때문에 프로그램의 코드가 유출되지 않는다.
- 컴파일 에러(문법적 에러)와 관련된 에러를 초기에 발견할 수 있다.
- 단점
- 코드를 수정하면 컴파일을 다시 해야 한다.
- 소스 파일 전체를 컴파일해야 하므로 용량이 크다. 따라서 수정 사항이 빈번할 경우 문제가 발생할 수 있다.
- 모든 소스 파일을 한꺼번에 번역하기 때문에 컴파일 시간이 비교적 느리다.
- 목적 파일 생성을 위해 메모리를 사용한다.
- 특정 시스템에서 만들어진 실행 파일이 다른 시스템에서는 실행되지 않는 경우가 많다.
인터프리터
- 장점
- 메모리를 사용하지 않는다.
- 시스템 간의 이식성이 뛰어나다.
- 전체 코드를 다시 컴파일할 필요가 없기 때문에 코드 수정에 용이하다.
- 단점
- 매번 번역 과정을 거쳐야 하기 때문에 실행 속도가 컴파일러에 비해 느리다.
- 중간 코드로 해석되기 때문에 프로그램의 코드가 유출될 수 있다.
Java의 컴파일러와 인터프리터
자바 코드를 실행하기 위해서는 아래와 같은 과정이 필요하다.
- 자바 파일(.java)을 자바 컴파일러(javac)를 통해 바이트코드 파일(.class)로 컴파일한다. 즉, 자바 컴파일러는 자바 소스코드를 JVM을 위한 기계어로 변환한다.
- JVM의 실행 엔진 내에 있는 자바 인터프리터를 통해 바이트코드를 특정 환경의 기계어로 번역하고 실행한다.
자바는 왜 자바 컴파일러를 사용할까?
자바는 WORA를 구현하기 위해 물리적인 머신과 별개의 가상 머신(JVM)을 기반으로 동작하도록 설계되었다. 그래서 자바 바이트코드를 실행하고자 하는 모든 하드웨어에 JVM을 동작시킴으로써 자바 실행 코드를 변경하지 않고도 모든 종류의 하드웨어에서 동작되게 한 것이다. Java 소스코드, 즉 원시코드(.java)는 CPU가 인식을 하지 못하므로 기계어로 컴파일을 해줘야 한다. 하지만 Java는 이 JVM이라는 가상 머신을 거쳐서 OS에 도달하기 때문에 OS가 인식할 수 있는 기계어로 바로 컴파일되는 게 아니라 JVM이 인식할 수 있는 자바 바이트코드(.class)로 변환된다. 자바 컴파일러가 .java 파일을 .class 파일인 자바 바이트코드로 변환한다.
JVM의 실행 엔진은 왜 인터프리터를 사용할까?
1. 인터프리터는 플랫폼에 종속되지 않는다.
바로 기계어로 변환하는 컴파일러의 경우는 프로그램이 작성된 기계상에서 매우 효율적으로 실행된다. 이는 대부분의 하드웨어 제어 시스템의 프로그래밍 언어가 C인 이유이다. 그러나 이와 동시에 기계에 종속된다는 말이기도 하다. 자바 인터프리터은 자바 컴파일러를 통해 생성된 바이트코드를 한 줄씩 읽어 기계어로 번역하고 실행한다. 따라서 이는 WORA를 구현하고자 하는 자바의 철학과 맞닿아있다. 만약 JVM 내에서 컴파일러를 사용하여 바이트코드의 목적 파일을 생성한다면 이는 기계에 종속되는 파일이기 때문이다. (하지만 이와 모순되게 인터프리터는 컴파일러보다 실행 속도가 너무 느리기 때문에 이를 해결하고자 JVM은 부분적으로 JIT 컴파일러를 사용하여 바이트코드를 컴파일하여 사용한다.)
2. 초기 실행 속도를 빠르게 할 수 있다.
자바 바이트코드 전체를 프로그램 수행 초기에 모두 읽어 컴파일하게 되면 초기 실행 속도가 느리다. 인터프리터를 사용하면 초기 실행 속도를 높일 수 있다.
3. 보안적으로 장점이 있다.
자바 바이트코드는 컴퓨터와 프로그램 사이에 별도의 버퍼 역할을 한다. 인터넷이나 기타 매체를 통해 신뢰할 수 없는 프로그램을 다운로드하여 실행하는 경우 어느 정도 안전이 보장될 수 있다. 자바 인터프리터를 사용함으로써 바이러스나 기타 악성 프로그램에 대응하는 가드 같은 보안 계층에 의해 보호될 수 있다.
JIT 컴파일러
앞서 말했듯이, 자바는 코드를 실행하기 위해서는 바이트코드로 컴파일하는 과정과 바이트코드를 인터프리트하는 과정을 거쳐야 하기 때문에 컴파일 과정만 필요한 다른 프로그래밍 언어보다 느리다. 거기에 더하여 인터프리터는 컴파일러보다 느리기 때문에 성능 문제기 발생할 수밖에 없었다.
이러한 문제를 개선하기 위해 나온 것이 JIT 컴파일러이다. 원래 자바의 JVM에서는 인터프리터 방식만 사용했다. 하지만 성능 문제가 발생했고 JIT 컴파일러를 추가해서 성능을 올리게 되었다.
JIT 컴파일러 동작 방식
JIT 컴파일러는 실행 시점에서는 인터프리터와 같이 기계어 코드를 생성하면서 해당 코드가 컴파일 대상이 되면 컴파일하고 그 코드를 캐싱한다. JIT 컴파일은 코드가 실행되는 과정에 실시간으로 일어나며(그래서 Just-In-Time이다), 전체 코드의 필요한 부분만 변환한다. 기계어로 변환된 코드는 캐시에 저장되기 때문에 재사용 시 컴파일을 다시 할 필요가 없다.
- JIT 컴파일러가 컴파일하는 조건은 얼마나 자주 코드가 실행됐는가 이다. 일정한 횟수만큼 실행되고 나면 컴파일 임계치에 도달하고 컴파일러는 컴파일하기에 충분한 정보가 쌓였다고 생각한다.
- 임계치는 메서드가 호출된 횟수, 메서드의 루프를 빠져나오기까지 돈 횟수 두 개를 기반으로 한다. 이 두 수의 합계를 확인하고 메서드가 컴파일될 자격이 있는지 여부를 결정한다. 자격이 있다면 메서드는 컴파일되기 위해 큐에서 대기한다. 이후 메서드들은 컴파일 스레드에 의해 컴파일된다.
- 아주 오랫동안 돌아가는 루프 문의 카운터가 임계치를 넘어가면 해당 루프는 컴파일 대상이 된다. JVM은 루프를 위한 코드의 컴파일이 끝나면 루프가 다시 반복될 때는 코드를 컴파일된 코드로 교체하고 더 빠르게 실행된다. 이 교체 과정을 "스택 상의 교체(on-stack replacement, ORS)"라고 부른다.
그림으로 보자면, 위와 같이 반복되는 코드들을 컴파일하여 캐싱해둠으로써 인터프리터는 반복되는 코드를 읽지 않고 컴파일된 코드를 바로 사용할 수 있는 것이다.
JIT 컴파일러의 이점
일반적인 인터프러터 언어는 바이트코드나 소스코드를 최적화 과정이 없이 번역하기 때문에 성능이 낮다. 반면 정적으로 컴파일하는 언어는 실행 전에 무조건 컴파일을 해야 하기 때문에 다양한 플랫폼에 맞게 컴파일을 하려면 시간이 오래 걸린다. JIT 컴파일러는 실행 과정에서 컴파일을 할 수 있기 위해 만들어졌다. JIT 컴파일러는 정적 컴파일러만큼 빠르면서 인터프러터 언어의 빠른 응답속도를 추구하기 위해 사용한다. 또한 바이트코드 컴파일러가 시간이 많이 소요되는 최적화를 미리 해주기 때문에 바이트코드에서 기계어 번역은 훨씬 빠르게 진행될 수 있어 성능상의 이점이 있다.
📗 참고
'Programming language > Java' 카테고리의 다른 글
[Java] SOLID란? (0) | 2022.05.16 |
---|---|
[Java] 객체지향이란? (0) | 2022.05.16 |
[Java] GC란? (0) | 2022.05.12 |
[Java] JVM, JRE, JDK (0) | 2022.05.12 |
[Java] JVM이란? (0) | 2022.05.05 |
댓글