JIT(Just-In-Time) 컴파일러란 무엇인가요?

김상욱·2024년 11월 17일

JIT(Just-In-Time) 컴파일러란 무엇인가요?

JIT(Just-In-Time) 컴파일러는 프로그램 실행 중에 바이트코드(Bytecode)를 기계어(Machine Code)로 동적으로 변환하여 프로그램을 실행하는 컴파일러.
: 동적으로 변환한다는 것은 JIT 컴파일러는 프로그램 실행 시점(runtime)에 코드의 실행 흐름을 분석. 또 모든 코드를 미리 컴파일하는 것이 아니라, 실제로 실행되는 코드만 동적으로 변환 -> 초기 실행 시간이 단축되고, 메모리를 효율적으로 사용.

주로 Java와 같은 가상 머신(JVM) 기반의 언어에서 사용되며, 프로그램의 실행 속도를 크게 향상시킬 수 있습니다.

JIT 컴파일러의 특징

  • 동적 컴파일 : 프로그램 실행 도중(runtime) 바이트코드를 기계어로 변환. 이 과정에서 핫스팟(Hotspot)이라고 불리는 자주 실행되는 코드 부분을 최적화.
  • 성능 최적화 : JIT 컴파일러는 실행 중에 런타임 정보를 수집하고 이를 바탕으로 인라인 확장, 루프 최적화, 불필요한 코드 제거등의 최적화 수행 -> 인터프리터 방식 보다 빠름
  • 플랫폼 의존적 : JIT 컴파일은 실행 중에 해당 플랫폼의 기계어로 변환되므로 플랫폼에 따라 최적화된 코드를 생성 -> ex) windows면 winodw가 해석하게 mac이면 mac이 해석하게
  • 캐싱 : JIT 컴파일러는 한 번 변환한 기계어 코드를 캐싱하여 동일한 코드가 반복 실행될 때 다시 컴파일x 바로 실행o

동작 과정
1. JVM이 .class 형태의 바이트코드를 메모리에 로드
2. 초기에는 인터프리터가 바이트코드를 한 줄씩 읽어 실행
3. JVM이 런타임 동안 자주 실행되는 코드(핫스팟)을 감지
4. 핫스팟 코드를 기계어로 변환하고 최적화 수행
5. 변환된 기계어 코드를 실행하여 성능을 향상

  • 런타임 최적화를 통해 반복 실행 시 성능이 향상, 각 플랫폼에 맞는 최적화된 기계어 코드 생성 가능
  • but, 컴파일 시간이 필요하기 때문에 프로그램 시작 시 느릴 수 있음, 또한 런타임 최적화와 캐싱으로 인해 메모리 사용량이 증가할 수 있음
  • 런타임 최적화 시에 실행 중 수집한 프로파일링 정보(Profiling Data : 메서드 호출 횟수, 분기별 실행 빈도, 변수 타입 정보 등)을 저장하기 때문에 메모리를 사용
  • 중간 표현(Intermediate Representation, IR) 즉, 바이트 코드를 기계어로 변환하기 전에 중간 단계를 사용하는데 이 중간 표현은 코드 분석, 최적화, 변환 등을 위해 사용하고 메모리를 추가로 사용
  • 인터프리터와 JIT 컴파일러가 동시에 사용되기 때문에 바이트코드와 캐싱된 기계어 코드가 동시에 메모리에 존재 -> 메모리 중복 사용
  • JIt 컴파일러가 기계어 코드를 생성하면서 메모리를 추가로 할당하면서 GC의 부담이 증가.


Java와 Spring 개발자를 위한 JIT 실습

1. JIT 컴파일 동작 확인: JVM 핫스팟 분석

  • 목적: JVM의 JIT 컴파일 과정을 확인하고, Spring 서비스 로직이 최적화되는 과정을 이해.
  • 실습 내용:
    • 자주 호출되는 서비스 메서드 또는 반복 작업 로직 작성.
    • JVM 옵션 -XX:+PrintCompilation을 사용하여 JIT 컴파일 과정을 확인.

예제 코드 (Spring 없이 기본 Java):

public class JITExample {
    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            System.out.println(process(i));
        }
    }

    public static int process(int x) {
        return x * x;
    }
}

실행 명령:

java -XX:+PrintCompilation JITExample

결과:

  • 콘솔 출력에서 JIT 컴파일러가 어떤 메서드를 컴파일했는지 확인 가능.
  • process 메서드가 반복 호출되면서 JIT에 의해 컴파일됨.

2. Spring 애플리케이션의 JIT 최적화 체험

  • 목적: Spring 기반 REST API에서 JIT 컴파일의 성능 최적화를 체험.
  • 실습 내용:
    • 간단한 REST API 작성.
    • 반복적으로 호출되는 컨트롤러 로직의 실행 속도를 확인.

예제 코드:

@RestController
@RequestMapping("/api")
public class JITController {

    @GetMapping("/compute")
    public int compute() {
        int result = 0;
        for (int i = 0; i < 100000; i++) {
            result += i * i;
        }
        return result;
    }
}

실행:
1. Spring Boot 애플리케이션 실행.
2. curl 또는 Postman으로 /api/compute 엔드포인트를 반복 호출.

curl http://localhost:8080/api/compute
  1. JIT 컴파일 동작을 분석하려면 JVM 옵션 추가:
    java -XX:+PrintCompilation -jar target/your-app.jar

결과:

  • 반복 호출되는 compute 메서드가 JIT에 의해 컴파일되어 실행 속도가 점점 빨라짐.

3. JIT과 JVM 옵션 비교

  • 목적: JIT 활성화/비활성화에 따른 Spring 애플리케이션의 성능 차이 확인.
  • 실습 내용:
    • JVM 옵션을 사용해 JIT 비활성화 후, 동일한 애플리케이션의 성능 차이를 측정.

실행 명령:
1. JIT 비활성화:

java -Xint -jar target/your-app.jar
  • -Xint: 순수 인터프리터 모드(인터프리터만 사용, JIT 사용 안 함).
  1. JIT 활성화(기본):
    java -jar target/your-app.jar

결과:

  • JIT 비활성화 시 초기 실행 속도는 비슷하지만, 반복 호출 시 성능이 떨어짐.
  • JIT 활성화 상태에서는 반복 호출 시 성능이 점점 최적화됨.

4. JIT과 메모리 관리 분석

  • 목적: JIT 최적화가 메모리 사용량에 미치는 영향을 이해.
  • 실습 내용:
    • VisualVM 또는 JConsole로 JVM 메모리 사용량 모니터링.
    • JIT 컴파일이 메모리 사용량을 증가시키는 패턴 확인.

실행 방법:
1. Spring Boot 애플리케이션 실행.
2. VisualVM 설치 후 애플리케이션 프로세스 선택.
3. Heap 메모리와 클래스 로딩 데이터를 분석.
4. JIT 활성화 상태에서 캐싱된 기계어 코드와 관련된 메모리 증가 확인.


5. JIT을 활용한 성능 최적화 사례 학습

  • 목적: JIT 최적화가 실제 애플리케이션에 어떤 영향을 미치는지 체험.
  • 실습 내용:
    • 복잡한 비즈니스 로직을 반복 실행하며, JIT 컴파일러의 최적화를 통해 성능 병목을 제거.
    • AOP(Aspect-Oriented Programming)와 같이 런타임에 동작하는 Spring 기능의 성능을 분석.

JIT 실습의 학습 효과

  1. JVM 내부 동작 이해
    • JIT 컴파일러가 핫스팟 코드를 최적화하는 원리를 학습.
  2. Spring 애플리케이션 성능 튜닝
    • JIT 관련 JVM 옵션을 활용하여 Spring 서비스의 성능 병목 해결.
  3. JIT과 메모리 트레이드오프 이해
    • 성능 최적화와 메모리 사용량 증가의 관계를 체감.

결론

Java와 Spring 개발자로서 JIT 컴파일러를 이해하고 관련 실습을 진행하면 JVM 기반 애플리케이션 성능 최적화에 대한 깊은 통찰을 얻을 수 있습니다. 특히, Spring 애플리케이션의 서비스 메서드, REST API, 반복 로직에서 JIT 최적화의 효과를 체험하며, 이를 통해 JVM 성능 튜닝의 기반 지식을 쌓을 수 있습니다.

0개의 댓글