Class Loading
JVM 프로세스가 시작하면, 필요한 모든 클래스들은 클래스로더가 세가지 단계를 거쳐 메모리로 로드 한다. 이 과정은 lazy loading 기반으로 동작한다.
- Bootstrap Class Loading: Bootstrap Class Loader 가 Java Class 들을 올린다. java.lang.Object 와 같이 JRE/lib/rt.jar 에 있는 필수적인 클래스를 올린다.
- Extension Class Loading: ExtClassLoader 는 java.ext.dirs 경로에 있는 모든 JAR 파일들을 책임진다. ㅎGradle 혹은 Maven 기반 application 이 아닌, 개발자가 수동으로 추가한 JAR 들이다.
- Application Class Loading: AppClassLoader 가 application class path 에 있는 모든 클래스들을 올린다.
Execution Engine
Java 는 Interpret 과 Compile 을 모두 사용하는 Hybrid 이다. Java code 를 javac 를 통해서 컴파일하면 platform 독립적인 bytecode 로 변환된다. 이를 실행 시 JVM 은 runtime 에 interpret 통해서 native code 로 실행한다. Just-In-Time Compiler 는 runtime 에 자주 실행되는 코드(method 전체)를 native code 로 compile 한다. 이후에 재실행 시 바로 compiled 된 native code 를 사용한다. 이를 hotspot 이라 부른다.
C1 - Client Compiler
빠른 시작과 좋은 반응 속도에 중점을 두고 있다. 최적화 수준은 C2 에 비해 낮지만 컴파일 시간이 짧아 초기 실행 속도를 빠르게 하여 사용자에게 더 빠른 반응을 제공할 수 있다. 복잡하지 않은 최적화로 짧은 시간에 코드를 컴파일하여, 어플리케이션이 빨리 실행될 수 있도록한다.
C2- Server Compiler
빠른 속도보다 성능 최적에 중점을 둔다. C1 보다 오래 걸리는 컴파일 과정을 통해 깊이 있는 최적화를 수행하여 실행 속도를 최대화한다. C1 에 비해 고급 최적화 기술을 사용하여 실행 시간 동안 코드의 실행 패턴을 분석하고, 이를 기반으로 더 코드를 더 효율적으로 만들어 장기적인 성능을 향상시킨다.
Tiered Compilation
Java8 부터는 기본적으로 활성화 되어 있으며, 기본 설정을 사용하는게 권장된다.
C2 컴파일러는 대개 C1 에 비해 더 많은 메모리와 시간을 사용한다. 하지만 C1 보다 더 최적화된 native code 를 제공한다. Java 7 에서 처음 도입되었다. 목표는 C1 과 C2 를 모두 사용하여 빠른 시작시간과 장기적인 성능 향상을 모두 도모한다.
인터프리터를 사용하여 method 의 프로파일링 정보를 수집하고 컴파일러에게 제공한다. C1 은 이를 통해 컴파일된 버전을 생성한다.
JVM 은 application life-cycle 동안 자주 사용되는 method 는 native cache 에 올라간다.
application 시작 시, JVM 은 처음에 모든 bytecode 를 인터프리팅하고 그에 대한 정보를 프로파일링한다. JIT 컴파일러는 수집된 프로파일링을 hotspot 을 찾는데 사용한다.
먼저, JIT 컴파일러는 빈번하게 실행되는 코드 섹션을 C1 이 빠르게 native code 로 컴파일한다. 이후에, C2 는 interpreter 와 C1 을 통해 생성된 프로파일링 정보를 사용하여 C1 이 컴파일한 native code 를 보다 더 최적화한다. 이 과정에서 C1 이 소요한 시간보다 더 소요된다.
Code cache
JVM 이 은 native code 로 컴파일된 모든 bytecode 를 저장하는 메모리 영역이다. Tiered Compilation 은 code cache 영역의 코드양을 4배로 상승시킨다.
Java9 이후로, code cache 를 3가지 영역으로 분할했다. 지역성을 개선하고 메모리 파편화를 줄여 성능 개선에 목표를 두었다.
- non-method segment - JVM 내부 관련 코드 (약 5MB, -XX:NonNMethodCodeHeapSize 를 통해 조정 가능)
- The profiled-code segment - C1 이 컴파일한 코드로 수명이 짧을 수 있다. (약 122MB 기본 설정 -XX:ProfiledCodeHeapSize 로 조정 가능)
- The non-profiled segment - C2 가 컴파일한 코드로 수명이 길 수 있다. (약 122MB 기본 설정 -XX:NonProfiledCodeHeapSize 로 조정 가능)
Deoptimazation
C2 가 컴파일한 코드가 최적화 되지 않을 가능성이 있다. 해당 경우 JVM 은 일시적으로 인터프리팅 방식으로 롤백한다. 예를 들어 프로파일 정보가 실제 method 실행과 일치하지 않는 경우.
Compilation level
인터프리터와 JIT 컴파일러는 5가지 단계가 존재한다.
level 0 - Interpreted Code
이 단계에서 JVM 은 모든 Java code 를 인터프리팅한다. 바이트코드를 한 줄씩 일고 실행하기에 이 단계에서는 컴파일 언어에 비해서 성능이 좋지 않다.
level 1 - Simple C1 Compiled Code
JVM 은 프로프일링 정보를 수집하지 않고 C1 을 사용하여 중요하지 않다고 판단되는 method 를 컴파일한다. 보통 매우 간단하거나 복잡성이 낮은 method 에 적용된다. 이러한 method 들은 C2 가 추가 최적화를 진행하여도 성능 향상이 기대되지 않는다. 주된 목적은 실행을 빠르게 하기 위해서이다. 최소한의 오버헤드로 코드를 실행할 수 있게 한다. 프로파일링 정보를 수집하지 않기에, JVM 은 이 단계에서 실행되는 코드에 대해 추가저인 최적화 여부를 결정하지 않는다. 시스템 리소스 사용을 줄이고 간단한 메서드에 대해 빠른 실행을 보장한다.
level 2 - Limited C1 Compiled Code
C1 은 경량 프로파일링을 통해 코드를 분석한다. JVM 은 C2 Queue 가 가득찼을 때 해당 단계를 사용한다. C2 는 많은 시간과 자원을 필요로 하는 광범위한 최적화를 수행하므로, 일시적으로 경량 프로파일링과 함께 C1 컴파일러를 사용하여 대기 시간 없이 성능을 개선한다.
level 3 - Full C1 Compiled Code
level 2 에서 컴파일된 코드를 일정 시간 실행한 후 JVM은 더 많은 runtime 데이터를 수집하고 해당 단계에서 C1 을 통해 full 프로파일링 컴파일한다. 경량 프로파일링보다 더 포괄적인 데이터 수집을 포함하여 복잡한 패턴과 최적화 기회를 식별할 수 있다. C2 가 수행할 더 복잡한 최적화를 위해 상세한 실행 메트릭을 수집한다.
level 4 - C2 Compiled Code
C2 Queue 가 가용하면서 level 3 의 full 프로파일링을 기반으로 중요한 핫스팟이 식별되면, 해당 단계가 진행된다. C2 가 최적화 기술을 적용하여 native code 를 생성한다. 최종 단계이며, 광범위한 프로파일링 데이터에서 얻은 통찰을 기반으로 실행 효율을 최대화하려고 한다.

Tier3CompileThreshold 에 도달할 때까지 JVM 은 인터프리팅 방식을 진행한다. 그 후 C1 은 method 를 컴파일하고 프로파일링도 계속 진행한다. 최종으로 C2 는 Tier4CompileThreshold 에 도달했을 때 컴파일을 진행한다. JVM 은 C2 컴파일 코드를 deoptimize 할 지 결정할 수 있다. 결정되면 처음부터 다시 진행된다.
JVM Warming up
클래스 로딩이 끝나면, 프로세스 시작 시 사용된 중요한 클래스들이 runtime 동안 보다 빠르게 동작할 수 있도록 JVM cache 로 들어간다. 이외의 다른 클래스들은 per-request 방식으로 요청 시 JVM cache 에 간다.
Lazy Class loading 과 Just In Time compilation 으로 인해서 Java web application 에서 첫 번째 request 는 평균 응답시간이 느리다.
첫 요청에서 느린 응답을 개선하기 위해서는 모든 클래스를 사전에 JVM cache 에 올려야 한다. 이 과정을 JVM warming up 이라고 한다.
Manual Implmentation
application 시작 시 사용 되는 클래스를 직접 사용하도록 사용자 클래스 로더를 작성하는 것이다. web application 의 경우 스스로 api 요청을 하도록 할 수 있다. Spring Boot 를 사용하는 경우, Spring LifeCycle 과정에서 CommandLineRunner 혹은 ApplicationRunner 를 사용하여 내부 호출을 진행할 수 있다.
1. ApplicationStartEvent
2. ApplicationEnvironmentPreparedEvent
3. ApplicationContextInitializedEvent
4. ApplicationPreparedEvent
5. ApplicationStartEvent
6. AvailabilityChangeEvent(LivenessState.CORRECT)
7. ApplicationRunner, CommandLineRunner 실행
- 내부 호출로 application 에서 사용되는 class 를 미리 로딩하여 native cache 에 올릴 수 있다.
8. ApplicationReadyEvent(ReadinessState.ACCEPTING_TRAFFIC)
참고
댓글
댓글 쓰기