About JVM Warm up

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)


참고

댓글

이 블로그의 인기 게시물

About idempotent

About Kafka Basic

About ZGC

sneak peek jitpack

Spring Boot Actuator readiness, liveness probes on k8s

About Websocket minimize data size and data transfer cost on cloud

About G1 GC

대학생 코딩 과제 대행 java, python, oracle 네 번째