"Spring application의 첫 요청이 느린 이유" 라는 내용으로 검색을 했을 때 Hotspot(JIT Compiler)에 대한 내용을 엄청 많이 볼 수 있다.
많이들 JIT Compiler로 알고 있는데, Oracle에서는 Hotspot이라고 부른다고 한다. 아래 Hot/Cold에 관해 이야기 할 예정이라 좀 더 직관 적인 Hotspot 이란 단어로 말하겠다.
Hotspot에 대해 말하기 전에 JVM 코드 실행을 한 번 정리하겠다.
1. jar, war 파일로 bulid 되면서 java 코드로 작성된 파일들은 byte 코드로 변환이 된다.
2. byte 코드로 변환된 코드는 ClassLoader에 의해 메모리에 올라간다.
3. 메모리에 올라 간 코드는 Execute Engine의 interpreter 에 의해 기계어로 변환되어 실행된다.
근데 우리가 알고 있는 Hotspot(JIT Compiler)는 언제 동작을 할까?
Hotspot(JIT Compiler) 동작
Interpreter 는 실행할 때마다 번역해야되는 단점이 있어서 Hotspot 이란 개념이 나왔다고 하는데, Hotspot은 언제 실행에 개입을 하는 걸까?
⇒ Hotspot는 대부분 Method 단위로 컴파일 하고 실행하는 Method가 Hot한 거 같으면 개입한다. 그래서 Hotspot이란 이름이 붙었다.
근데 Hot하다는 것의 기준은 뭘까?
먼저 Interpreter, Hospot(C1, C2) 에는 Profiler라는 것이 존재하고 실행횟수, 루프의 반복 횟수 등 최적화에 필요한 값들을 저장한다.
수집한 값들로 특정 Threshold를 넘었는지 확인하고 넘었다면 해당 Method가 Hot하다고 판단하고 최적화를 진행한다.
Hotspot은 C1(Client Complier), C2(Server Complier) 라는 이름으로 총 2개가 존재한다. 간단하게 C1은 간략한 최적화, C2는 최대 최적화를 진행하고 JVM Memory의 Code Cache에 컴파일된 코드를 저장해 둔다.
또, Interpreter, C1 Compiler, C2 Compiler 를 5개의 Level로 나눈다.
*C1에서만 3가지로 세분화 된다.
Level 0 - 인터프리터 (Interpreter)
초기에 모든 byte code를 interprete 방식으로 실행할 때마다 해석한다.
Level 1 - C1 컴파일러 (Full Optimization, No Profiling)
C2 Compiler을 해도 최적화 되지 않을만한 아주 간단한 메서드에 한해서 Level 1 컴파일을 한다.
이 단계에서는 Profiling을 하지 않는다.
Level 2 - C1 컴파일러 (Invocation and Backedge Counters)
이 단계는 C2 컴파일러로 가는 Queue의 사이즈를 보고 Level 2 컴파일을 할지 Level 3 컴파일을 할지 결정한다.
일반적으로 Level 2 컴파일러가 Level 3 컴파일러보다 30% 빠르다고 한다. C2 컴파일러 큐가 사이즈가 많이 차있을 경우 Level 2로 컴파일해서 Level 3에 있는 시간을 최소화한다. C2 컴파일러의 큐가 줄어들면 Level 3로 가서 Profiling 정보를 수집한다.
빠른 이유는 Level 3 에선 Level 2 보다 Profiling 정보를 많이 모으기 때문에 일 거 같다. 큐 사이즈가 너무 커서 빠르게 C2 최적화를 할 수 없으니 Level 2에서 최적화 해서 당장 Method를 임시로 빠르게 하는 방식으로 생각된다.
Level 3 - C1 컴파일러 (Full Profiling, Level 2 정보 및 모든 Profiling 정보)
C2 컴파일러에서 가장 극한의 최적화를 하기 위해 엄청 자세하게 Profiling을 진행한다.
일반적으로 Level 0에서 Level 3로 오는 것이 일반적이다.
Level 4 - C2 컴파일러 (Full Profile-Guided Optimization, No Profiling)
C2 컴파일러가 여태 수집한 Profile 정보로 최고 수준의 최적화를 수행한다.
Runtime 환경에서 쌓은 정보를 바탕으로 최적화를 하기 때문에 단순 AOT 컴파일(e.g. C, C++ 컴파일 방식) 방식보다 좋은 성능의 기계어 코드 생산이 가능하다.
하지만, 프로파일링 정보를 바탕으로 optimistic 가정이 잘못되었다면 레벨 0으로 되돌려 보낸다.
공부를 하면서 초기엔 왜? 단계적으로 최적화를 하는지 의아했다. 결국엔 C2 Compiled 하려고 그러는거 아닌가? 라는 생각이 들었는데, 결국엔 Trade-off 때문이다.
모든 코드를 C2로 컴파일하여 캐시에 올리는 것이 과연 최선일까? 캐시 용량에 문제가 생기진 않을지, 정작 필요한 코드가 캐시에서 밀려나는 상황은 없을지 생각이 든다. 또, 충분한 프로파일링 없이 컴파일을 수행하면 극한의 최적화가 가능할까?
결국 안정성과 성능 등 다양한 요소들 간의 Trade-off를 고려했기 때문에 이런 방식으로 만들어졌다는 생각이 든다.
공부를 하다보면 좋은 점만 부각되게 배운다. 예를 들어서 지금 Hotspot 같은 경우는 Oracle에서 만든거고 Oracle 공식 문서를 참고해서 공부했는데, 굳이 자기가 만든 제품의 단점을 도드라지게 얘기할까? 아닐 것이다. 기술의 장점과 단점을 파악해서 어떤 Trade-off가 있는지 판단할 수 있는 능력을 기르는 것이 중요한 것 같다.
하지만 그럼에도 불구하고, 공부를 하는 내내 byte code로 변환하는 작업이 너무 불필요하게 느껴졌다. Java는 한 번 build 하고 다양한 환경에서 모두 실행할 수 있도록 하자라는 컨셉으로 만들어졌지만, 나의 경우에는 거의 Server application(e.g. Spring) 용도로 사용하는데 Docker라는 가상화 기술도 있고 Cloud 환경에 올릴거라 실행하는 곳의 환경이 명확하다. 그래도 byte code가 필요할까?!
이 컨셉으로 생겨난 게 바로 GraalVM이다.
연관 게시물
- Spring application의 첫 요청이 느린 이유, JVM 파해치기 (ClassLoader)
- Spring application의 첫 요청이 느린 이유, JVM 파해치기 (Hotspot)
- Spring application의 첫 요청이 느린 이유, JVM 파해치기 (GraalVM)
Ref
https://github.com/openjdk/jdk/blob/master/src/hotspot/share/compiler/compilationPolicy.hpp
https://www.baeldung.com/jvm-tiered-compilation
https://www.youtube.com/watch?v=CQi3SS2YspY
'Java' 카테고리의 다른 글
C2 Compiler를 대체 하기 위한 Graal JIT, Cold Start를 피하기 위한 AOT Compile 지원, JVM 파해치기 (GraalVM) (0) | 2024.11.15 |
---|---|
JVM 첫 요청이 느린이유, JVM 파해치기 (ClassLoader) (0) | 2024.11.02 |
내부 클래스 (0) | 2023.11.11 |
접근 제어자(access modifier) (0) | 2023.04.02 |
Comparable(compareTo)과 Comparator(compare) (0) | 2023.02.17 |