JVM 이 클래스 파일을 컴퓨터에서 실행하기 위해서는 네이티브 코드로 해석해야 한다.
JVM 명세는 바이트 코드의 해석 및 실행의 구현을 명세하지 않는다.
따라서 JVM 들은 각자의 방식으로 이를 구현할 수 있는데, 가장 많이 사용되는 HotSpot 에서는 세가지 방식으로 사용할 수 있다.
인터프리터는 클래스 파일의 내용을 그대로 해석 실행하는것에 비해
JIT, AOT 컴파일러는 바이트코드에서 네이티브 코드로 해석하는 과정에서 많은 최적화가 이뤄진다.
Hotspot 코드를 발견하면 그때그때 컴파일하여 실행(Just-In-Time)

https://www.infoq.cn/article/java-10-jit-compiler-graal
HotSpot JVM 의 JIT 컴파일러는 C1, C2 가 있다.
C1, C2, 인터프리터는 각자 다른 최적화 레벨을 가지고 있다.
실행중인 바이트 코드를 시스템 상태와 호출 횟수 등에 따라 최적화 레벨을 달리하여 실행하는데, 이를 계층형 컴파일 이라 한다.
인터프리터 : 최초 HotSpot 구동시 인터프리터 모드로 실행C1(클라이언트 컴파일러) : C2 보다 빠른 컴파일, 간단한 최적화.C2(서버 컴파일러, Opto) : C1 보다 느린 컴파일, 공격적인 최적화(높은 런타임 성능)위 그림과 같이 보자.
-XX:+TieredCompilation : 계층형 컴파일 활성화(JDK 7 부터 기본값). 인터프리터, C1, C2 모두 사용-XX:-TieredCompilation : 계층형 컴파일 비활성화. 인터프리터&C2 사용-client 로 C2 대신 C1 으로 고정 가능-XX:+TieredCompilation -XX:TieredStopAtLevel=1 : 인터프리터&C1 사용-XInt : 인터프리터만 사용-XComp : JIT 컴파일러만 사용. 시작 시간이 오래 걸릴 수 있다.java -version
java -XInt -version
java -XComp -version
를 실행시켜 보자. 각각 Mixed mode, interpreted mode, compiled mode 를 볼 수 있다.
디버그를 위해 JIT 컴파일러 동작을 배제하고 싶은 경우 -XInt 로 인터프리터만 쓸 수 있고,
AWS lambda 같이 빨리 끝나고 가벼운 애플리케이션 실행의 경우 C2 를 배제할 수도 있고,
C2 를 대체한 Graal Compiler 를 사용하려는 경우 C1 을 배제할 수도 있다.
JIT 컴파일러의 컴파일 대상이 되는 코드 유형은 두가지가 있다.
이 기준은
인터프리터 -> C1이다.
C1 -> C2는 복잡한 모니터링 결과에 의해 전환된다.
HotSpot 은 카운터를 기반으로 핫스팟 코드를 감지한다.
JIT 컴파일 대상 코드 유형이 2가지이므로, 카운터도 2가지가 존재한다.
-XX:CompileThreshold 로 설정 가능-XX:-UseCounterDecay 감쇠 카운터 비활성화. 호출 카운터를 감쇠하지 않고 절대값으로 측정-XX:CounterHalfLifeTime 반감기를 초 단위로 설정 가능
(메서드 호출 카운터 임계값 * OSR 비율)/100(메서드 호출 카운터 임계값 * OSR 비율 - 인프라 모니터링 비율)/100
위 다이어그램 두개를 비교해 보면 back edge 카운터에 의한 OSR 컴파일 요청은 메서드 호출 카운터와 다른점이 있다.
back edge 카운터는 C1 컴파일이 요청된 후에 back edge 카운터 값을 줄인다는 것이다.
둘 다 공통적으로 임계값을 넘겼을 때 컴파일이 요청되었으면 더 이상 요청하지 않아야 할 것이다.
단 OSR 컴파일의 경우 반복문 안에서 그 동작이 발생하기 때문에, 컴파일이 요청되었는가를 체크하는 일 자체가 메서드 호출 카운터에 비해 훨씬 부담되는 일이다.
따라서 back edge 카운터에 의한 OSR 컴파일 요청은 호출 횟수가 임계값을 넘은 경우 호출 횟수를 일정량 줄인다. 다시 임계치에 오르는 동안 요청이 되었는지 체크를 하지 않을 수 있다.
컴파일은 기본적으로 메서드 컴파일, OSR 컴파일 둘 다 백그라운드 스레드에서 비동기로 진행.
-XX:-BackgroundCompilation 옵션으로 동기적으로 컴파일하도록 변경 가능. 이러면 컴파일 진행중 일시 정지됨.

https://nothingcosmos.github.io/OpenJDKOverview/src/overview.html
C1 컴파일에서 바이트 코드는 다음의 과정을 거쳐 네이티브 머신 코드로 변환된다.
BytesCode -> HIR -> LIR -> Native Code(Machine Code)
위 그림을 보면 컴파일 과정에서 HIR, LIR 각각에 적용되는 최적화들이 다름을 볼 수 있다.
IR (Intermediate Representation)
최적화를 위해 컴파일 과정에서 중간에 변환되는 중간 코드.
HIR(High IR)은 좀 더 바이트 코드와 비슷한 고수준의 중간 언어이며, 코드 레벨의 최적화를 하기에 적합한다.LIR(Low IR)은 좀 더 기계어와 비슷한 중간 언어이며, 기계어 수준의 최적화를 하기 적합하다.바이트코드는 네이티브 머신 코드가 되기 전 중간 코드(IR. Intermediate Representation)를 거친다. JIT 컴파일 과정의 최적화는 모두 IR 을 대상으로 이뤄진다.
C2 는 C1 컴파일 과정에서 수집된 모니터링 정보를 토대로, 공격적인 예측 최적화 를 적용한다.
예를 들면 다음과 같은 최적화들을 진행한다. C2 컴파일의 컴파일 과정은 상당히 복잡하다.
공격적 예측 최적화
확실하지 않지만 모니터링 결과를 토대로 어떠한 상황임을예측, 가정한 후 최적화하는 것.
가정을 확실하게 알기 위해서는 비용이 많이 들기에 지금까지의 모니터링 결과로 예측하는 트레이드 오프를 적용한 결과다.대부분의 상황은 자바가 가상 메서드이기 때문에 발생. 런타임에 리시버가 확정되지 않아 최적화하기 힘듦. 모니터링의 결과로 대부분이 하나의 리시버로 사용됨을 알게 되면, 미래에도 하나의 리시버일 것임을 가정한 후 최적화. 다른 리시버가 들어오면 인터프리터로 실행.
해당 가정이 틀린 경우는 interpreter 모드로 도망가서 일단 실행한 후 다시 컴파일한다.
-XX:+PrintCompilation : 컴파일 로그-XX:+PrintCompilation -XX:+PrintInlining : 인라이닝 포함 컴파일 로그-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly : 컴파일러 어셈블리 코드-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+PrintOptoAssembly : 좀 더 최종 결과에 가까운 중간 코드 표현(서버 모드 가상 머신용)-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+PrintLIR : 좀 더 최종 결과에 가까운 중간 코드 표현(클라이언트 모드 가상 머신용)-XX:PrintIdealGraphLevel=2 -XX:PrintIdealGraphFile=ideal.xml : IGV 에서 컴파일러 보기 위함
// javac Test.java
// java -XX:+PrintCompilation Test
public class Test {
public static final int NUM = 15000;
public static int doubleValue(int i) {
for(int j = 0; j < 100000; j++); // JIT 코드 최적화 과정 시연용 '빈' 순환문
return i * 2;
}
public static long calcSum() {
long sum = 0;
for (int i = 1; i <= 100; i++) { // doubleValue()를 100번 호출
sum += doubleValue(i);
}
return sum;
}
public static void main(String[] args) {
for (int i = 0; i < NUM; i++) { // calcSum()을 15,000번 호출
calcSum();
}
}
}
java -XX:+PrintCompilation Test 결과
190 306 % 3 servercompiler.Test::doubleValue @ 2 (18 bytes)
194 307 % 4 servercompiler.Test::doubleValue @ 2 (18 bytes)
194 306 % 3 servercompiler.Test::doubleValue @ 2 (18 bytes) made not entrant
194 308 4 servercompiler.Test::doubleValue (18 bytes)
195 309 3 servercompiler.Test::calcSum (26 bytes)
195 310 % 4 servercompiler.Test::calcSum @ 4 (26 bytes)
196 311 4 servercompiler.Test::calcSum (26 bytes)
198 309 3 servercompiler.Test::calcSum (26 bytes) made not entrant
183 303 % 3 servercompiler.Test::doubleValue @ 2 (18 bytes)
184 304 3 servercompiler.Test::doubleValue (18 bytes)
184 305 % 4 servercompiler.Test::doubleValue @ 2 (18 bytes)
185 303 % 3 servercompiler.Test::doubleValue @ 2 (18 bytes) made not entrant
185 305 % 4 servercompiler.Test::doubleValue @ 2 (18 bytes) made not entrant
185 306 4 servercompiler.Test::doubleValue (18 bytes)
185 304 3 servercompiler.Test::doubleValue (18 bytes) made not entrant
185 307 % 4 servercompiler.Test::doubleValue @ 2 (18 bytes)
185 308 3 servercompiler.Test::calcSum (26 bytes)
186 308 3 servercompiler.Test::calcSum (26 bytes) made not entrant
186 309 3 servercompiler.Test::calcSum (26 bytes)
186 310 % 4 servercompiler.Test::calcSum @ 4 (26 bytes)
187 311 4 servercompiler.Test::calcSum (26 bytes)
188 309 3 servercompiler.Test::calcSum (26 bytes) made not entrant
Ideal Graph Viewer.
JIT 컴파일 과정에서 코드는 Ideal Graph 라는 형태로 표현된다.
IGV 플러그인을 사용하면 이 그래프를 컴파일 단계별로 시각적으로 볼 수 있다. 이를 이용하면 컴파일 과정에서 최적화가 어떻게 되는지 시각적으로 볼 수 있다.
IGV 는 JDK 소스 코드에 이미 포함되어 있다. 다만 제공되는 런타임에는 없기에, 소스코드를 받아서 직접 빌드하여 실행하도록 한다.
IGV 는 JDK 소스코드에 포함. 빌드하여 사용
$ cd src/utils/IdealGraphVisualizer
$ mvn install
$ chmod u+x igv.sh
$ ./igv.sh


이 그림에서는 빨간부분이 위 테스트 코드 doubleValue() 의 빈 순환문이며, 없어졌다는 뜻이다.