JVM은 어떻게 구동될까?


웹 기반 시스템을 배포할 때 그냥 재시작만 한다면,
배포 직후 시스템 사용자들은 엄청나게 느린 응답 시간과 함께 시스템에 대한 많은 불만을 갖게 될 수도 있다.
즉, warning up이 필요한데 왜 이런 작업이 필요할까?
왜 WAS을 재시작하면 성능이 느린지 알아보자.


다음은 반드시 성능과 관련있는 부분은 아니지만 아래 내용은 알아두면 좋다.


HotSpot VM 은 어떻게 구성되어 있을까?

자바의 성능을 개선하기 위해 Just In Time(JIT) 컴파일러를 만들었고, 이름을 HotSpot이라고 지었다.

JIT 컴파일러는 프로그램의 성능에 영향을 주는 지점에 대해서 지속적으로 분석한다.
분석된 지점은 부하를 최소화하고, 높은 성능을 내기 위한 최적화의 대상이 된다.

JIT를 사용한다는 것은 언제나 자바 메서드가 호출되면
바이트 코드를 컴파일하고 실행 가능한 네이티브 코드로 변환한다는 의미다.
하지만 매번 JIT로 컴파일을 하면 성능 저하가 심하므로, 최적화 단계를 거치게 된다.


HotSpot VM 아키텍처는 다음과 같다.

KakaoTalk_Photo_2021-07-09-16-25-48


HotSpot VM 런타임GC 방식JIT 컴파일러를 끼워 맞춰 사용할 수 있다.
이를 위해 VM 런타임은 JIT 컴파일러용 API와 가비지 컬렉터용 API를 제공한다.
JVM을 시작하는 런처와 스레드 관리, JNI 등도 VM 런타임에서 제공한다.




JIT Optimizer라는 게 도대체 뭘까?

HotSpot VM JIT 컴파일러에 대해서 이야기하기 전에
Client 버전과 Server 버전으로 나뉜다는 것을 기억하자. client는 스타트업 시간과 메모리공간에 대한 최적화에 중점을 두고 있고,
server는 시작시간은 좀 걸리더라도 다수의 request를 빠르게 처리하는데에 중점을 맞추고 있다.


javac 컴파일러는 소스코드를 바이트코드로 된 class 파일로 변환시켜준다.
자바 프로그램을 실행할 때 JVM은 항상 바이트코드로 시작, 동적으로 기계에 의존적인 코드로 변환한다.

JIT은 애플리케이션에서 각각의 메서드를 컴파일할 만큼 시간적 여유가 없어서
모든 코드는 초기에 인터프리터에 의해서 시작되고 해당 코드가 충분히 많이 사용될 경우에 컴파일할 대상이 된다.


• 인터프리터
자바 인터프리터는 JAVAC 명령으로 자바 프로그램을 중간 형태인 자바 바이트코드로 컴파일하고,
이를 자바 인터프리터가 한 줄씩 해석하여 기계어로 번역한다.

• 컴파일
고급언어로 작성된 프로그램을 목적프로그램으로 번역 후 링킹(Linking) 작업을 통해 실행 프로그램을 생성한다.
자바는 javac로 컴파일 하고 java로 실행시 중간언어(클래스파일)를 한줄 씩
자바 인터프로터가 번역하므로 컴파일 언어이면서 인터프리터 언어이다.

• 왜 컴파일과 인터프리터 방식을 병행할까?
원래 자바의 JVM에서는 인터프리터 방식만 사용했었다.
하지만 성능의 문제가 발생으로 JIT Compiler를 추가해서 성능의 효율을 끌어올렸다.
이로 인해 자바는 컴파일과 인터프리터 방식을 병행해서 사용하게 됐다.


HotSpot VM에서 이 작업은 각 메서드에 있는 카운터를 통해서 통제되며,
메서드에는 두 개의 카운터가 존재한다.

backedge counter는 invocation counter보다 컴파일 우선순위가 더 높다.

이 카운터들이 인터프리터에 의해 증가될 때마다 그 값들이 한계치에 도달했는지 확인하고
도달했을 경우 인터프리터는 컴파일을 요청한다.

컴파일이 요청되면 컴파일 대상 목록의 큐에 쌓이고, 하나 이상의 컴파일러 스레드가 이 큐를 모니터링한다.
컴파일러 스레드가 바쁘지 않을 때는 큐에서 대상을 빼내서 컴파일을 시작한다.

HotSpot VM은 OSR(On Stack Replacement) 이라는 특별한 컴파일도 수행한다.
이 OSR은 인터프리터에서 수행한 코드 중 오랫동안 루프가 지속되는 경우,
중간에 컴파일해야 남은 반복을 빠르게 수행할 수 있기 때문에 사용된다.

최적화되지 않은 코드가 수행되고 있는 것을 발견하면 인터프리터에 두지 않고, 컴파일된 코드로 변경한다.
루프가 끝나지 않고 지속적으로 수행되고 있을 경우에 큰 도움이 된다.

SUN에서 개발한 HotSpot VM을 제외하고도 BAE 시스템에서 개발한 JRockit,
IBM에서 개발한 IBM JVM의 JIT 컴파일 방식이 있다.

JVM에 대한 더 많은 내용을 다음 글을 참고하면 좋을 것이다. JVM 에 대한 Naver D2 글




JVM 시작 절차

(1) java 명령어 줄에 있는 옵션 파싱

(2) 자바 힙 크기 할당 및 JIT 컴파일러 타입 지정 (명령줄에 지정되지 않았을 경우)

(3) CLASSPATH와 LD_LIBRARY_PATH 같은 환경 변수를 지정한다.

(4) 자바의 Main 클래스가 지정되지 않았으면, Jar 파일의 mainfest 파일에서 Main 클래스를 확인한다.

(5) JNI의 표준 API인 JNI_CreateJavaVM를 사용하여 새로 생성한 non-primordial이라는 스레드에서 HotSpot VM을 생성한다.

(6) HotSpot VM이 생성되고 초기화되면, Main 클래스가 로딩된 런처에서는 main() 메서드의 속성 정보를 읽는다.

(7) CallStaticVoidMethod는 네이티브 인터페이스를 불러 HotSpot VM에 있는 main() 메서드가 수행된다.
이떄 자바 실행 시 Main 클래스 뒤에 있는 값들이 전달된다.




JVM 종료 절차

VM이 시작할 때 오류가 있어 시작을 중지할 때나, JVM에 심각한 에러가 있어서 중지할 필요가 있을 때는
DestroyJavaVM이라는 메서드를 HotSpot 런처에서 호출한다.

HotSpot VM의 종료는 다음의 DestroyJavaVM 메서드의 종료 절차를 따른다.

(1) HotSpot VM이 작동중인 상황에서는 단 하나의 데몬이 아닌 스레드가 수행될 때까지 대기한다.

(2) java.lang 패키지에 있는 Shutdown 클래스의 shutdown() 메서드가 수행된다.
이 메서드가 수행되면 자바 레벨의 shutdown hook이 수행되고,
finalization-on-exit 이라는 값이 true일 경우에 자바 객체 finalizer를 수행한다.

(3) HotSpot VM 레벨의 shutdown hook을 수행함으로써 HotSpot VM의 종료를 준비한다.
이 작업은 JVM_OnExit() 메서드를 통해서 지정된다.
그리고, HotSpot VM의 profiler, stat sampler, watcher, garbage collector 스레드를 종료시킨다.
이 작업들이 종료되면 JVMTI를 비활성화하며, Signal 스레드를 종료시킨다.

(4) HotSpot의 JavaThread::exit() 메서드를 호출하여 JNI 처리 블록을 해제한다.
그리고, guard pages, 스레드 목록에 있는 스레드들을 삭제한다.
이 순간부터는 HotSpot VM에서는 자바 코드를 실행하지 못한다.

(5) HotSpot VM 스레드를 종료한다. 이 작업을 수행하면 HotSpot VM에 남아 있는 HotSpot VM 스레드들을 safepoint로 옮기고, JIT 컴파일러 스레드들을 중지시킨다.

(6) JNI, HotSpot VM, JVMTI barrier에 있는 추적 기능을 종료시킨다.

(7) 네이티브 스레드에서 수행하고 있는 스레드들을 위해서 HotSpot의 “vm exited” 값을 설정한다.

(8) 현재 스레드를 삭제한다.

(9) 입출력 스트림을 삭제하고, PerfMemory 리소스 연결을 해제한다.

(10) JVM 종료를 호출한 호출자로 복귀한다.




클래스 로딩 절차

(1) 주어진 클래스의 이름으로 클래스 패스에 있는 바이너리로 된 자바 클래스를 찾는다.

(2) 자바 클래스를 정의한다.

(3) 해당 클래스를 나타내는 java.lang 패키지의 Class 클래스의 객체를 생성한다.

(4) 링크 작업이 수행된다. 이 단계에서 static 필드를 생성 및 초기화하고, 메서드 테이블을 할당한다.

(5) 클래스의 초기화가 진행되며, 클래스의 static 블록과 static 필드가 가장 먼저 초기화 된다.
해당 클래스가 초기화 되기 전에 부모 클래스의 초기화가 먼저 이루어진다.




내부 클래스 로딩 데이터의 관리

HotSpot VM은 클래스 로딩을 추적하기 위해 다음 3개의 해시 테이블을 관리한다.




예외는 JVM에서 어떻게 처리될까?

JVM은 자바 언어의 제약을 어겼을 때 예외(exception)라는 시그널로 처리한다.
HotSpot VM 인터프리터, JIT 컴파일러, 다른 HotSpot VM 컴포넌트는 예외 처리와 모두 관련되어 있다.

일반적인 예외 처리 경우는 아래 두 가지 경우다.

후자의 경우에는 보다 복잡하며, 스택을 뒤져서 적당한 핸들러를 찾는 작업을 필요로 한다.


VM이 예외가 던져졌다는 것을 알아차렸을 때, 해당 예외를 처리하는 가장 가까운 핸들러를 찾기 위해서
HotSpot VM 런타임 시스템이 수행된다.

핸들러를 찾기 위해 다음 3가지 정보가 사용된다.

만약 현재 메서드에서 핸들러를 찾지 못하면 현재 수행되는 스택 프레임을 통해
이전 프레임을 찾는 작업을 수행한다.
적당한 핸들러를 찾으면 HotSpot VM 수행 상태가 변경되며 HotSpot VM은 핸들러로 이동하고 자바 코드 수행은 계속된다.