JIT(Just In Time Compiler)

wnajsldkf·2022년 10월 30일
1

Java

목록 보기
14/19
post-thumbnail

Java 코드는 어떻게 실행될까?

Java 코드는 두 번의 컴파일 과정을 거쳐 실행된다.

첫번째로 컴파일 환경에서 JAVAC Compiler에 의해 자바 코드(.java)를 바이트 코드(.class)로 변환하는 변환되며, 두번째로 JVM 상에서 런타임 환경에서 바이트 코드를 기계어(Native Code)로 변환시킨다.

이때 바이트코드를 기계어로 번역하는 컴파일러가 JIT 컴파일러이다.

정적 컴파일과 동적 컴파일

프로그램 코드가 기계어로 변환되는 시점은 크게 프로그램 실행 전, 프로그램 실행 중으로 구분된다.

프로그램 실행 전에 기계어로 변환하는 것을 정적 컴파일이라 하고, 프로그램 실행 중에 기계어로 변환하는 것을 동적 컴파일이라 한다.

정적 컴파일을 사용하는 언어는 C, C++이 있다. 이 방식은 컴파일 타임에 시간이 많이 걸리지만 프로그램 실행 전 기계어로 변환되었기 때문에 런타임 시 속도가 빠르다는 장점이 있다.

동적 컴파일을 사용하는 언어는 Python, Javascript이다. 이 방식은 컴파일 시간이 거의 필요없이 바로 실행할 수 있지만, 런타임 시 한줄한줄 읽으면서 실행되므로 프로그램 성능이 떨어진다.

JIT 컴파일러

이 한계를 극복하기 위해 설계된 컴파일러가 바로 JIT 컴파일러이다.

컴파일 단계에서 변환된 바이트 코드는 Execution Engine에 의해 실행된다. Excecution Engine은 Interpreter, JIT(Just In Time) 컴파일러로 구성되었다. Interpreter는 ByteCode를 기계가 이해할 수 있는 코드로 바꾸는데 바이트 코드를 한줄씩 읽고 실행하기 때문에, 속도 문제가 발생한다. 이런 문제를 극복할 수 있는 JIT 컴파일러이다.

JIT 컴파일러는 실행 중에 컴파일(동적 컴파일)을 한다. 하지만 코드의 빈도를 파악해 자주 사용되는 코드는 기계어로 변환하여 캐시에 저장한 후 재사용하기 한다. 따라서 동적 컴파일을 하는 언어보다 좋은 성능을 보여준다. 캐시에 저장된 코드를 사용하는 것은 미리 컴파일된 코드를 사용하는 것(정적 컴파일)과 같은 효과를 보여주기 때문이다.

그럼 얼마나 자주 호출되는지, 즉 코드의 빈도는 어떤 기준으로 측정하는 것일까? 그 기준은 바로 컴파일 임계치에 의해 판단된다.

컴파일 사용 옵션과 컴파일 레벨 체계

Java의 JIT Compiler는 C1(Client Compiler), C2(server Compiler)로 구분된다. C1은 level 1~3까지, C2는 level 4를 담당한다. Java8 전에는 각각의 컴파일러를 사용하기 위해 -clinet , -server flag를 두었다. 하지만 Java8 이후부터는 그럴 필요없이 사용할 수 있다.

C1 - Client Compiler

서버 시간보다 먼저 컴파일을 시작하고, 최적화를 위한 대기 시간이 짧다. 따라서 코드 실행이 서버보다 빠른 특징이 있다. level 1~3 코드는 C1 컴파일러로 기계어를 컴파일만 수행하고 캐싱은 수행하지 않는다.

C2 - Server Compiler

성능을 향상시키기 위해 최적화된 JIT 컴파일러 유형이다. C2는 C1에 비해 더 긴 시간동안 코드를 관찰하고 분석하여 더 나은 최적화를 사용한다. 서버 컴파일러는 모든 코드를 컴파일하지 않는다. level 4 코드는 C2 컴파일러로 기계어 컴파일 뿐 아니라 기계어 캐싱(컴파일된 코드를 캐싱)까지 수행한다.

+) C2 컴파일러 대안으로 Graal JIT 컴파일러를 Java10부터 사용할 수 있다고 한다.

Tiered Compiler(C1 + C2)

클라이언트 컴파일러와 서버 컴파일러의 장점을 조합한 컴파일러이다. 클라이언트 컴파일러로 스타트업 시간을 빠르게하고, 자주 사용되는 코드는 서버 컴파일러로 캐싱해둔다.

예를 들어 다음과 같은 코드가 있다고 하자.

public static void main(String[] args){
	functionA();   // C1 Compile
	functionA();   // C1 Compile
	functionA();   // C2 Compile + 캐시 저장
}

private static void functionA(){
}

컴파일 임계치(Compile Threshold)

컴파일 임계치는 다음 두가지에 의해 결정된다. 두 합계를 확인하여 메서드가 컴파일 될 자격을 얻으면 컴파일을 위한 큐에 대기하고, 컴파일 스레드에 의해 컴파일 된다.

  • method entry counter(JVM 내에 있는 메서드 호출 횟수)
  • back edge loop counter(메서드가 루프를 빠져나오기까지 돈 횟수)

컴파일 임계치는 옵션에 따라 클라이언트(-client, C1), 서버(-server, C2)로 구분된다. 클라이언트 컴파일러에서 기본 값은 1,500, 서버 컴파일러의 기본 값은 10,000이다.

Reference

profile
https://mywnajsldkf.tistory.com -> 이사 중

0개의 댓글