자바를 만든 Sun에서 자바 성능 개선을 위해 JIT 컴파일러를 만들고 HotSpot이라 명명했다. JIT 컴파일러가 프로그램 성능에 영향을 주는 HotSpot 에 대해서 지속적으로 분석하고 최적화하기에 HotSpot이라 명명하였다.
HotSpot VM은 세 가지 주요 컴포넌트가 있다.
위 그림에서 보듯 VM 런타임에 다양한 GC 방식과 JIT을 골라 끼울 수 있는 구조를 가지고 있다. 골라 끼울 수 있도록 하기 위해서 VM 런타임은 JIT 컴파일러용 API와 가비지 컬렉터용 API를 제공한다.
그 외 JVM을 시작하는 런처와, 스레드 관리, JNI 등도 VM 런타임에서 제공한다.
JVM은 바이트 코드를 읽어들여 동적으로 기계에 의존적인 코드로 변환하는 방식이다.
JIT는 메서드를 컴파일할 만큼 시간적 여유는 없다. 따라서 모든 코드는 인터프리터에 의해서 시작되고, 해당 코드가 충분히 많이 사용될 경우, 즉 HotSpot 일 경우에 컴파일할 대상이 된다. HotSpot VM에서 이 작업은 각 메서드에 있는 카운터를 통해서 통제되며, 메서드에는 두 개의 카운터가 존재한다.
백에지 카운터는 루프가 존재하는지를 확인할 때 사용한다. 수행 카운터보다 컴파일 우선순위가 높다.
각 카운터들이 한계치에 도달하면 컴파일을 하게된다.
CompileThreshold
에 도달하면 컴파일을 하고,CompileThreshold * OnStackRelaceRecentage / 100
에 도달하면 컴파일한다.JVM 시작시, 다음과 같이 지정할 수 있다.
- XX:CompileThreshold=35000
- XX:OnStackReplacePercentage=80
이렇게 지정할 경우, 메서드가 35000번 호출되었을 때 JIT에서 컴파일하며, 백에지 카운터는 35000 * 0.8 = 28000번 호출되었을 때 컴파일한다.
컴파일이 요청되면 컴파일 대상 목록 큐에 쌓이고, 하나 이상의 컴파일러 스레드가 이 큐에서 대상을 빼내서 컴파일한다. 인터프리터에서는 컴파일이 종료될 때까지 기다리지 않으나, -Xbatch
나 -XX:-BackgroundCompilation
옵션을 지정하여 컴파일을 기다리도록 할 수 있다.
HotSpot VM은 OSR이라는 특별한 컴파일도 수행한다. 오랫동안 루프가 지속되는 경우에 사용하는 것으로, 만약 해당 코드의 컴파일이 완료된 상태에서 최적화되지 않은 코드가 수행되고 있는 것을 발견한 경우에 인터프리터에 계속 머무르지 않고 컴파일된 코드로 변경한다.
예시로 JRockit JIT 컴파일 동작 방식을 알아보자.
JIT-Compiled machine code
로 컴파일한다. 애플리케이션이 시작하는 동안 몇천 개의 새로운 메서드가 수행되면 JRockit은 새로 시작하는 메서드를 JIT 컴파일하느라 다른 JVM보다 JRockit JVM이 시작은 더 느릴 수도 있다. 그러나 지속적으로 수행할 때는 JRockit이 더 빠른 처리가 가능하다.
모든 메서드를 컴파일하고 최적화하는 작업은 JVM 시작을 느리게 하기 때문에 시작시 모든 메서드를 최적화하지는 않는다.
주기적으로 애플리케이션의 스레드를 점검하여, 최적화 대상을 모니터링하는 스레드가 있다.
모니터링을 통하여 식별한 대상을 Highly optimized machine code
로 최적화한다. 이 작업은 백그라운드에서 진행되며 수행중인 애플리케이션에 영향을 주지 않는다.
IBM JVM의 JIT 컴파일 방식은 5가지로 나뉜다.
메서드가 단순할 때 적용되는 방식이다. 인라이닝은 callee 함수를 caller 함수에 넣어 함수 콜스택을 줄이는 것이다.
작은 단위의 코드를 분석하고 개선하는 작업을 수행한다.
메서드 내의 조건 구문을 최적화하고, 효율성을 위해서 코드의 수행 경로를 변경한다.
메서드 전체를 최적화한다. 매우 비용이 비싼 방식이며, 컴파일 시간이 많이 소요된다. 성능 개선이 많이 될 수 있다는 장점이 있다.
이 방식은 플랫폼 아키텍처에 의존적이다. 다시 말해서 아키텍처에 따라서 최적화를 다르게 처리하는 것을 말한다.
컴파일한 코드는 코드 캐시라고 하는 JVN 프로세스 영역에 저장된다. 결과적으로 JVM 프로세스는 JVM 수행 파일과 컴파일된 JIT 코드의 집합으로 구분된다.
CLASSPATH
와 LD_LIBARARY_PATH
등 환경 변수 지정JNI_CreateJavaVM
을 사용하여 새로 생성한 non-primordial
스레드에서 HotSpot VM을 생성main()
메서드의 속성 정보를 읽는다CallStaticVoidMethod
는 네이티브 인터페이스를 불러 HotSpot VM에 있는 main()
메서드가 수행된다. 이때 자바 실행 시 Main 클래스 뒤에 있는 값들이 전달된다.JNI_CreateJavaVM
단계 세부 절차JNI_CreateJavaVM
동시에 두개의 스레드에서 호출할 수 없고, 오직 하나의 HotSpot VM 인스턴스가 프로세스 내에서 생성할 수 있도록 보장한다. HotSpot VM이 정적인 데이터 구조를 생성하기 때문에 다시 초기화는 불가능하다. 따라서 오직 하나의 HotSpot VM이 프로세스에서 생성될 수 있다.
JNI 버전 호환성 점검 및 GC 로깅 준비
OS 모듈들 초기화. 예들 들어 랜덤 번호 생성기, PID 할당 등이 여기에 속한다.
커맨드 라인 변수와 속성들이 JNI_CreateJavaVM
변수에 전달되고, 나중에 사용하기 위해서 파싱후 보관
표준 자바 시스템 속성 초기화
동기화, 메모리, safepoint
페이지와 같은 모듈 초기화
libzip
, libhip
, libjava
, libthread
와 같은 라이브러리들 로드
시그널 처리기 초기화 및 설정
스레드 라이브러리 초기화
출력 스트림 로거 초기화
JVM 모니터링 에이전트 라이브러리가 설정되어 있으면 초기화 및 시작
스레드 처리를 위해서 필요한 스레드 상태와 스레드 로컬 저장소 초기화
HotSpot VM의 글로벌 데이터들이 초기화, 글로벌 데이터에는 이벤트 로그, OS 동기화, 성능 통계 메모리, 메모리 할당자들이 있다.
HotSpot VM에서 스레드를 생성할 수 있는 상태가 된다. main
스레드가 생성되고, 현재 OS 스레드에 붙는다. 그러나 아직 스레드 목록에 추가되지는 않는다.
자바 레벨의 동기화가 초기화 및 활성화
부트 클래스로더, 코드 캐시, 인터프리터, JIT 컴파일러, JIN, 시스템 dictionary, 글로벌 데이터 구조의 집합인 universe 등 초기화
스레드 목록에 자바 main
스레드 추가, universe 상태 점검. HotSpot VM의 중요한 기능을 하는 HotSpot VMThread가 생성된다. 이 시점에 HotSpot VM의 현재 상태를 JVMTI에 전달
java.lang
패키지에 있는 String
, System
, Thread
, ThreadGroup
, Class
클래스와 java.lang
하위 패키지인 Method
, Finalizer
클래스 등이 로딩되고 초기화된다.
HotSpot VM의 시그널 핸들러 스레드가 시작되고, JIT 컴파일러가 초기화되며, HotSpot의 컴파일 브로커 스레드가 시작된다. HotSpot VM과 관련된 각종 스레드들이 시작한다. 이때부터 HotSpot VM의 전체 기능이 동작한다.
JNIEnv
가 시작되며, HotSpot VM을 시작한 호출자에게 새로운 JNI 요청을 처리할 상황이 되었다고 전달
OS의 kill -9
시그널로 죽이면 해당 절차를 밟지 않음을 유의하자.
DestroyJavaVM
메서드를 HotSpot 런처에서 호출한다. HotSpot VM의 종료는 다음의 DestroyJavaVM
메서드의 종료 절차를 따른다.
java.lang
패키지에 있는 Shutdown
클래스의 shutdown()
메서드가 수행된다. 이 메서드가 수행되면 자바 레벨의 shutdown hook이 수행되고, finalization-on-exit
이라는 값이 true
일 경우에 자바 객체 finalizer
를 수행한다.JVM_OnExit()
메서드를 통해서 지정. gc 스레드 등 종료한 이후JVMTI를 비활성화하고 시그널 스레드를 종료JavaThread::exit()
메서드를 호출하여 JNI 처리 블록을 해제한다. 이 대부터 HotSpot VM은 자바 코드 수행을 할 수 없다.vm exited
값을 설정한다.PerfMemory
리로스 연결을 해제한다.java.lang
패키지의 Class
클래스의 객체를 생성한다.static
필드를 생성 및 초기화하고, 메서드 테이블을 할당static
블럭과 static
필드가 가장 먼저 초기화 된다. 해당 클래스가 초기화되기 전에 부모 클래스 초기화가 선행된다.클래스 로더가 클래스를 찾고 로딩할 때 다른 클래스 로더에 클래스를 로딩해달라 하는 경우가 있는데 이를 class loader delegation이라 한다. 클래스 로더는 계층적으로 구성되어 있으며, 기본 클래스 로더는 시스템 클래스 로더라고 불리며 main
메서드가 있는 클래스와 클래스 패스에 있는 클래스들이 이 클래스 로더에 속한다.
JVM은 자바 언어의 제약을 어겼을 때 예외라는 시그널로 처리한다. HotSpot VM 인터프리터, JIT 컴파일러 및 다른 HotSpot VM 컴포넌트는 예외 처리와 모두 관련되어 있다. 일반적인 예외 처리의 경우는 아래 두 가지 경우다.
후자의 경우는 보다 복잡하며, 스택을 뒤져서 적당한 핸들러를 찾는 작업이 필요하다.
예외는
1. 던져진 바이트 코드에 의해서 초기화 될 수 있으며
2. VM 내부 호출의 결과로 넘어올 수도 있고
3. JNI 호출로 부터 넘어올 수도 있고
4. 자바 호출로부터 넘어올 수도 있다
VM이 예외가 던져졌다는 것을 알아차렸을 때, 해당 예외를 처리하는 가장 가까운 핸들러를 찾기 위해서 HotSpot VM 런타임 시스템이 수행된다. 이 때, 핸들러를 찾기 위해서는 다음의 3개의 정보가 사용된다.
만약 현재 메서드에서 핸들러를 찾지 못했을 때는 현재 수행되는 스택 프레임을 통해서 이전 프레임을 찾는 작업을 수행한다. 적당한 핸들러를 찾으면, HotSpot VM 수행 상태가 변경되며, HotSpot VM은 핸들러로 이동하고 자바 코드 수행을 계속된다.
https://sungjk.github.io/2019/04/16/java-performance-tuning-4.html
자바 성능 튜닝 이야기 16장