Java

자바의 실행과 JIT(Just-In-Time) 컴파일러

변위니 2025. 1. 8. 10:51
 

자바 컴파일러를 통해 자바 소스 파일을 JVM이 이해할 수 있는 클래스 파일(.class/바이트코드)로 변환한다.

 

자바 바이트 코드

JVM이 이해할 수 있는 언어로 변환된 자바 소스 코드
자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1 바이트여서 자바 바이트 코드라 한다.
자바 바이트 코드는 자바 가상 머신만 설치되어 있으면, 어떤 운영체제에서도 실행될 수 있다.

 

 

 




그 후에는 어떻게 할까?

JVM의 Excution Engine 안에 있는 인터프리터를 통해 바이트 코드를 특정 환경의 기계어로 번역하고 실행한다.

 

 

컴파일러와 인터프리터

컴파일러와 인터프리터 모두 고수준 언어를 기계어로 변환시킨다.

컴파일러는 소스코드 전체를 한 번에 번역하여 중간 코드로 만들어 메모리에 적재하는 방식이다.
인터프리터는 소스코드를 한 줄 씩 중간 코드로 번역 후 실행한다.

컴파일러

  • 컴파일이 완료된 실행 파일은 컴퓨터에서 빠르게 실행할 수 있어 효율적이다.
  • 기계어로 번역되기 때문에 프로그램 코드가 유출되지않는다.
  • 컴파일 에러와 관련된 에러를 초기에 발견할 수 있다.
  • 코드 수정시 다시 컴파일 해야 한다.
  • 컴파일 시간이 비교적 느리며, 수정이 빈번한 경우 문제가 발생할 수 있다.

인터프리터

  • 메모리를 사용하지 않으며, 시스템 간 이식성이 좋다.
  • 전체 코드를 다시 컴파일할 필요가 없기 때문에 코드 수정에 용이한다.
  • 매번 번역 과정을 거쳐야 하기 때문에 실행 속도가 느려 컴파일러에 비해 느리다.
  • 중간 코드로 해석되기 때문에 프로그램의 코드가 유출될 수 있다.

 

 

 


Just-In-Time 컴파일러

자바는 코드를 실행하기 위해서는 바이트코드로 컴파일하는 과정과, 바이트코드를 인터프리터하는 과정을 거쳐야 한다.

 

바이트 코드도 결국에는 기계가 이해해야 하기 때문에 기계어로 번역이 되어야 한다. 그래서 인터프리터를 사용해서 런타임 시에 한 줄씩 읽어 실행한다. 따라서 런타임 전 소스코드를 미리 읽어 기계어로 변환하는 순수 컴파일 방식보다 느릴 수 밖에 없다.

 

이를 개선하기 위한 것이 JIT 컴파일러이다.

자주 사용되는 바이트 코드를 기계어로 번역한 뒤, 캐시에 저장하여 재사용하는 방법이다. 따라서 매번 기계어로 변환할 필요없이 시간을 단축시킬 수 있어 정적 컴파일과 같은 효과를 낼 수 있다.

 

하지만 모든 코드를 캐시하는 건 아니고, 내부적으로 자주 사용되는 코드를 선별해서 캐시 공간에 넣어둔다.

 

 

 

 

각 메서드의 호출 횟수를 추적하고, 호출 횟수가 C1의 임계값을 초과하면 해당 메소드를 C1 컴파일러 대기열에 넣고 재컴파일하여 최적화한다. 이후에도 계속 각 메서드의 호출 횟수를 추적하여 동일하게 최적화를 진행하는데, C1 이후에는 C2 컴파일러를 사용한다. 이렇듯 컴파일러를 순차적으로 적용하는 방식을 계층형 컴파일(Tiered Compliation)이라고 한다.

 

 

 


C1, C2 Compiler

JVM의 JIT 컴파일러 내부에는 2가지 컴파일러인 C1컴파일러와 C2컴파일러가 있다.

C1은 level1~3, C2는 level4를 담당한다.

 

C1 - Client Compiler

C1 컴파일러의 주 목표는 빠른 시작 시간(Fast Startup)이다. JVM이 애플리케이션 실행 초기에 인터프리터의 성능을 보완하며, 가벼운 최적화를 통해 가능한 빨리 기계어로 컴파일하여 프로그램이 빠르게 실행되도록 돕는 데 중점을 둔다.

데스크탑 어플리케이션, GUI 어플리케이션과 같은 짧은 실행 시간과 빠른 응답속도가 중요한 프로그램에 적합하다. 

최적화보다는 컴파일 속도를 빠르게하는 것이 목표이다. 

 

C2 - Server Compiler

C2의 주 목표는 최대 실행 성능(Maximum Execution Performance)이다.

컴파일 속도보다는 최적화를 통해 CPU, 메모리 사용을 줄이는 것을 목표로하는 컴파일 방식이다. 

성능이 중요한 서버 환경에서 사용되며, 장기 실행되는 프로그램에 적합하도록 설계되었다.  실행 빈도가 높은 코드(Hot Code)를 분석하고 최적화 적용하여 애플리케이션이 장시간 실행될 때 최고 수준의 성능을 유지할 수 있도록 한다.

이를 위해 실행 중에 더 많은 프로파일링 데이터를 수집하여, 이 데이터를 기반으로 정교한 최적화를 수행한다.

하지만 최적화 과정으로 인해 컴파일 시간이 상대적으로 길어 프로그램의 시작 시간이 느려질 수 있다. 따라서 C2 컴파일러는 장기 실행되는 서버 애플리케이션에서 더 적합하다.

 

 

 


 

Tiered Compilation

C1, C2 컴파일러의 장점을 결합한 JVM 컴파일 전략으로 JVM은 Tiered Compile를 지원한다.

Tiered Compile는 인터프리터로 실행되다가 C1 컴파일 형식으로 바뀌고, 이를 C2가 고급 최적화를 수행하는 방식으로 단계를 바꾸는 방식이다.

애플리케이션이 실행될 때, C1 컴파일러가 먼저 작동하여 빠른 시작을 보장하고, 코드가 자주 실행되어 Hot Code로 판변되면, C2가 해당 코드를 최적화하여 장기적인 실행 성능을 확보하는 방식이다. 

 

총 5가지 레벨이 존재한다.

레벨0 : 인터프리터(Interpreter) 사용
레벨1 : C1 컴파일러 (프로파일링 사용X)
레벨2 : C1 컴파일러 (프로파일링 부분 이용 - 호출카운터, 백엣지 카운터)
레벨3 : C1 컴파일러 (풀 프로파일링)
레벨4 : C2 컴파일러

 

JVM은 실행 빈도, 상태에 따라 메서드를 단계적으로 컴파일하며, 상황에 따라 C1과 C2 컴파일러의 역할과 우선순위가 조정된다.
 
JVM은 모든 메서드를 처음에 바이트코드 상태(0레벨)로 두고, 인터프리터를 통해 실행하고, 실행 빈도가 잦아지면 컴파일 단계를 거친다.

 

C1은 빠르게 실행하기 위해 메서드를 간단하게 최적화해서 컴파일 한다. 이 과정에서 프로파일링 데이터를 수집한다.

C2는 고급 최적화를 통해 메서드를 컴파일 한다. 만약 C2가 가득 차면, JVM은 C1에서 간단히 컴파일하거나 2레벨로 내려간다. 그리고 다시 C2가 여육 생기면 고급 최적화를 시도한다.

반대로 C1이 가득 차면 일부 메서드가 바로 2레벨로 컴파일되고, 이후 C2에서 최적화 된다.

 

자주 실행되지 않거나, 최적화가 더 필요하지 않은 경우에는 다시 레벨 0으로 복귀한다. 이걸 역 최적화라고 한다.

 

프로파일러(Profiler)란?
Profile은 프로그램이 실행될 때, 실행 시간, 메모리 사용량, 함수 호출 횟수 등과 같은 정보를 수집하여 프로그램의 동작을 분석하는 과정으로, 프로파일링을 통해 프로그램의 병목 지점을 찾고, 최적화할 부분을 결정한다.

프로파일러(Profiler)는 프로그램의 실행 정보를 수집하여 분석하는 도구로, Java의 VisualVM이 있다.

 

 

 

 


AOT (Ahead-of-Time) Compiler

C/C++ 와 같이 정적 컴파일을 해주는 컴파일러

프로그램 코드를 실행하기 전 미리 기계어로 변환하는 방식으로, 실행 시점에 추가적인 컴파일 작업이 필요하지 않아 실행 속도가 빠르다.
하지만, 특정 플랫폼에 종속적이며, 다른 환경에서 실행하려면 별도로 AOT 컴파일 해야 한다.



 

 

 


참고

'Java' 카테고리의 다른 글

Stack 사용을 왜 지양해야 할까?  (0) 2025.01.20
GC(Garbage Collection)에 대하여  (2) 2025.01.09
JVM 내부 동작 원리  (1) 2025.01.08
SQL Injection  (0) 2025.01.07
HashMap의 구조  (0) 2025.01.07