본문 바로가기
책을 읽겠습니다!/자바 성능 튜닝 이야기

[자바 성능 튜닝 이야기] JVM은 도대체 어떻게 구동될까?

by Unagi_zoso 2023. 9. 15.

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

Java HotSpot Performance Engin. 자바를 만든 Sun에서 성능 개선을 위해 JIT 컴파일러를 만든 것. HotSpot은 자바 1.3부터 기본 VM으로 사용되어왔ㄷ사.

HotSpot VM은 세 가지 주요 컴포넌트로 되어 있다.

  • VM(Virtual Maching) 런타임
  • JIT 컴파일러
  • 메모리 관리자

JIT Just In Time. 언제나 자바 메서드가 호출되면 바이트 코드를 컴파일 하고 실행 가능한 네이티브 코드로 변환한다..하지마 매번 JIT로 컴파일을 하면 성능저하가 심해, 최적화 돤계를 거치게 된다.

HotSpot VM 아키텍쳐를 보면 HotSpot VM 런타임에 GC 방식과 JIT 컴파일러를 끼워 사용한다. 그 사이에 API를 제공한다.

JIT Optimizer라는 게 도대체 뭘까?

HotSpot VM JIT 컴파일러는 서버, 클라이언트 버전 나뉜다.
C언어를 보면 컴파일한 후 object 파일이 되고 이 파일들로 수행 가능한 라이브러리를 만든다. 이 작업은 애플리케이션이 수행되는 것과 비교해 한 번만 수행한다.

자바는 javac라는 컴파일러를 사용한다. 소스코드를 바이ㅡ 코드로 된 class 파일로 변환한다. 그렇기에 JVM은 항상 바이트 코드로 시작한다.
JIT는 애플리케이션에서 각각의 메서드를 컴파일할 만큼 시간적 여유가 많지 않다. 그러므로, 모든 코드는 초기에 인터프리터에 의해 시작되고, 해당 코드가 충분히 많이 사용될 경우에 컴파일할 대상이 된다. HotSpot VM에서 이 작업은 각 메서드에 있는 카운터를 통해서 통제되며, 메서드에 있는 두 개의 카운터가 존재한다.

  • 수행 카운터(invocation counter): 메서드를 시작할 때마다 증가
  • 백에지 카운터(backedge counter): 높은 바이트 코드 인덱스에서 낮은 인덱스로 컨트롤 흐름이 변경될 때마다 증가.. 메서드에 루프가 존재하는지를 확인할 때 사용되며 수행 카운터보다 컴파일 우선순위가 높다.

이 카운터들이 인터프리터에 의해서 증가될 때마다 , 그 값들이 한계치에 도달했는지 확인하고, ㄷㅗ달했을 경우 이너프리터는 컴파일을 요청한다. 여기서 수행 카운터에서 사용하는 한계치는 CompileThreshold이며, 백에지 카운터의 한계치는

CompileThreshold * OnStackReplacePercentage / 100

이 두 값들은 JVM이 시작할 떄 지정 가능하다.

  • XX:CompileThreashold=35000
  • XX:OnStackReplacePercentage=80

컴파일 요청 오면 큐에 쌓이고, 하나 이상의 컴파일러 스레드가 이 큐를 모니터링한다.. 만약 스레드가 바쁘지 않을 때 큐에서 대상을빼내 컴파일을 시작한다. 보통 인터프리터는 컴파일이종료되기를 기다맂지 않는 대신, 수행 카우ㄴ터를 리셋하고 인터프리터에서 메서드 수행을 계속한다. 컴파일이 종료되면, 컴파일된 코드와 메서드가 연결되어 그 이후부터는 메서드가 호출되면 컴파일된 코드를 사용하게 된다.

HotSpot VM은 OSR(OnStackReplacement)라는 특별한 컴파일도 수행한다. OSR은 인터프리터에서 수행한 코드 중 오랫동안 루프가 지속되는 경우 사용된다. 콛의 컴파일이 완료된 상태에서 최적화 되지 않은코드가 수행되고 이쓴ㄴ걸 발견하면 인터프리터에 머무르지 않고 컴파일된 코드로 변경한다. 이 작업은 인터프리터에서 시작된 오랫동안 지속되는 루프가 다시는 불리지 않을 경우엔 도움이 되지 않지만, 루프가 끝나지 않고 지속적으로 수행되고 있을 경우 큰 도움이 된다.

Java 5 HotSpot VM의 새로운 기능 JVM 시작 시 플랫폼과 시스템 설정을 평가해 자동으로 GC 선정, 자바 힙, JIT 컴파일러를 선택. 애플리케이션 활동, 객체 할당 비율에 따라 GC가 동적으로 자바힙 크기 조절..

  1. 최적화 단계에서 final로 선언된 메서드 인라인 ㅊ리한다,. 메서드 호출로 인한 성능 저하가 개선된다.
  2. 불필요한 부하 제거, 값이 동일한 걸 받으면 함 이어 받게 한다.
  3. 복제, z, y 값이 동일하므로 변수 통일...?
  4. 죽은 코드 삭제. 필요없는 코드는 삭제한다.

JRockit 컴파일러용 스레드 하나 살다가 기회볼때 컴파일링 한다.. 벤더마다 다 다르다

IBM JVM의 JIT 컴파일 및 최적화 절차.

1) 인라이닝
- 메서드가 단순할ㄹ 때 적용되는 방식. 단순하면 코드에 포함해 버린다. 자주 호출되는 메서드의 성능이 향상된다.

2) 지역 최적화
- 작은 단위의 코드를 분석하고 개선하는 작업을 수행한다.

3) 조건 구문 최적화
- 메서드 내의 조건구문을 최적화하고, 효율성을 위해서 코드의 수행 경로를 변경한다.

4) 글로벌 최적화
- 메서드 전체를 최적화하는 방식. 매우 비싸고 컴파일 시간 많이 소요, 성능 개선 많이 될 수 있다.

5) 네이티브 코드 최적화
- 이 방식은 플랫폼 아키텍처에 의존적이다.

컴파이된 코드는 '코드 캐시'라고 하는 JVM 프로세스 영역에 저장된다. 결과적으로 JVM 프로세스는 JVM 수행 파일과 컴파일된 JIT 코드의 집합으로 구분된다.

JVM 시작 시 절차

간단히 java 명령을 ㅗ HelloWorld라는 클래스를 실행하면 다음과 같은단계로 수행된다.

1) java 명령어 줄에 있는 옵션파싱:
일부 명령은 적절한 JIT 컴파일리 선택 등을 위해, 다른 명령들은 HotSpot VM에 전달

2) 자바 힙 크기 할당 및 JIT 컴파일러 타입 지정: (이 옵션들이 명령줄에 지정되지 않을 경우) 메모리 크기나 JIT 컴파일러 종류가 명시적으로 지정되지 않은 경우에 자바 실행 프로그램이 시스템의 상황에 맞게 선정한다. 이 과정은 좀 박잡한 단계(HotSpot VM Adaptive Tuning)를 거치니 일단 패스

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

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

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

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

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

5)의ㅣ VM 생성하는 JNI_CreateJavaVM 단계에 대해서 더 알아보자.

1) JNI_CreateJavaVM은 동시에 두 개의 스레드에서 호출할 수 없고, 오직 하나의 JotSpotVM 인스턴스가 프로세스 내에서 생성될 수 있도록 보장된다. HotSpot VM 인스턴스가 프로세스 내에서 생성될 수 있도록 보장된다. NotSpot VM이 정적인 데이터 구조를 생성하기 때문에 다시 초기화는 불가능하기 때문에, ㅇ오직 하나의 HotSpotVM이 프로세스에서 생성될 수 있다.

2) JNI 버전이 호환성이 있는지 점검하고, GC 로깅을 위한준비도 완료된다.

3) OS 모듈들이 초기화된다. 예를 들면 랜덤 번호 생성기, PID 할당 등이 여기에 속한다.

4) 커맨드 라인 변수와 속성들이 JNI_CreateJavaVM 변수에 전달되고, 나중에 사용하기 위해서 파싱한 후 보관한다.

5) 표준 자바 시스템 속성 (properties)이초기화된다.

6) 동기화, 메모리, safepoint 페이지와 같은 모듈들이 초기화 된다.

7) libzip, libhpi, libjava, libthread와 같은 라이브러리들이 로드된다.

8) 시그널 처리기가 초기화 및 설정된다.

9) 스레드 라이브러리가 초기화된다.

10) 출력 스트림 로거가 초기화된다.

11) JVM을 모니터링하기 위한 에이전트 라이브러리가 설정되어 있으면 초기화 및 시작된다.

12) 스레드 처리를 위해서 필요한 스레드 상태와 스레드 로컬 저장소가 초기화된다.

13) HotSpot VM의 '글로벌 데이터'들이 초기화된다. 글로벌 데이터에는 이벤트 로그, OS 동기화, 성능 통계 메모리, 메모리 할당자 들이 있다.

14) HotSpot VM에서 스렏를 생성할 수 있는 상태가 된다. main 스레드가 생성되고, 현재 OS 스레드에 붙는다. 그러나 아직 스레드 목록에 추가되지는 안흔다.

15) 자바 레벨의 동기화가 초기화 및 활성화된다.

16) 부트 클래스로더, 코드 캐시, 인터프리터, JIT 컴파일러, JNI, 시스템 딕셔너리, 글로벌 데이터구조의 지합인 universe 등이 초기화된다.

17) 스레드 목록에 바자 main 스레드가 추가되고, universe 등의 상태를 점검한다. HotSpot VM의 중요한 기능을 하는 HotSpot VMThread가 생성된다. 이 시점에 HotSpotVM의현재 상태를 JVMTI에 전달한다.

18) java.lang 패키지에 있는 String, System, Thread, ThreadGroup, Class 클래스와 java.lang의 하위 패키지에 있는 Method, Finalizer 클래스 등이 로딩되고 초기화된다.

19) HotSpot VM의 시그널 핸들러 스레드가 시작되고 JIT 컴파일러가 초기화되며, HotSpot의 컴파일 브로커 스레드가 시작된다. 그리고, HOtSPot VM과 관련된 각 종 스레드들이 시작한다. 이 떄부터 HotSPot VM의 전체적인 기능이 동작한다.

20) JNIEnv가 시작되며, HOTSPot VM을 시작한호출자에게 새로운JNI 요청을 처리할 상황이 되었다고 전달해준다.

이렇게 복잡한 JNI_CreateJavaVM시작단계를 거치고 , 나머지 단계들을 거치면 JVM이 시작된다.

JVM이 종료될 떄의 절차는 이렇다.

그러면 JVM이 종료될 때는 어떤 절차? 정상적으로 종료될때의 이야기다. OS의 kill -q와 같은 명령으로 종료시 이 절차 따르지 않는다.

만약 JVM이 시작할 때 오류가 있어 싲가을 중지할 때나, JVM에 심각한 에러가 이어서 중지할 필요가 있을 떄 DestroyJavaVM이라는 메서드를 HotSpot 런처에서 호출한다.
HotSpotVM의 종료는 다음의 DestroyJavaVM메서드의 종료 절차를 따른다.

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

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

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

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

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

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

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

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

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

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

이 절차로 JVM이 종료된다.

무척 어렵다. 외울 필요는 없고 한 번 머리에 훑어보자..

클래스 로딩 절차도 알고 싶어욧

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

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

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

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

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

loading -> linking -> initializing으로 진행된다.

클래스 로더가 클래스를 찾고 로딩할 때 다른 클래스 로더에 클래스를 로딩해달라고하는 경우가 있따. 이를 'class loader delegation'이라고 부른다. 클래스 로더는 계층적으로 구성되어 있다. 기본 클래스 로더는 '시스템 클래스 로더'라고 불리며 main 메서드가 있는 클래스와 클래스 패스에 있는 클래스들이 이 클래스 로더에 속한다. 그 하위에 있는 애플리케이션 클래스 로더는 자바 SE의 기본 라이브러리에 있는 것이 될수도 개발자가 임의로 만든 것일 수도 있따.

부트스트랩 클래스 로더를 HotSPot VM은 구현한다. 부트스트랩 클래스로더는 HotSpot VM의 BOOTCLASSPATH에서 클래스 로드한다. JavaSE 클래스 라이브러리들을 포함하는 rt.jar가 여기에 속한다.

HotSPot의 클래스 메타데이터

HotSpot VM 내에서 클래스를 ㄹ딩하면 클래스에 대한 instanceKlass와 arrayKlass라는 내부적인 형식을 VM의 perm 영역에 생성한다. instanceKlass는 클래스의 정보를 포함하는 java.lang.Class 클래스의 인스턴스를 말한다. HotSPot VM은 내부 데이터 구조인 KlassOop라는 것을 사용하여 내부적으로 instanceKlass에 접근한다. 여기서 Oop라는 것은 ordinary object pointer의 약자다. KLassOop는 클래스를 나타내는 포인터인 셈

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

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

  • SystemDictionary
    롣된 클래스를 포함하며, 클래스 이름 및 클래스 로더를 키를 갖고 그 값으로 KlassOop를 갖고 있다. SystemDictionary는 클래스 이름과 초기화한 로더의 정보, 클래스 이름과 정의한 로더의 정보도 포함한다. 이 정보들은 safepoint에서만 제거된다.

  • PlaceholderTable
    현재 로딩된 클래스들에 대한 정보를 관리한다. 이 테이블은 ClassCircularityError를 체크할 때 사용ㅇ하며, 다중 스레드에서 클래스를 로딩하는클래스 로더에서도 사용된다.

  • LoaderConstraintTable
    타입 체크시의 제약 사항을 추정하는 용도로 사용된다.

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

언어의 제약을 어겼을때 예외(exception)라는 시그널(signal)로 처리한다. HotSpot VM 인터프리터, JIT 컴파일러 및 다른 HotSpot VM 컴포넌트는 예외처리와 모두 관련되어 있다. 일반적인 예외처리 경우 아래 두가지 경우다.

  • 예외를 발생한 메서드에서 잡을 경우
  • 호출한 메서드에 의해서 잡힐 경우

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

  • 던져진 바이트 코드에 의해서 초기화 될 수 있으며,
  • VM 내부 호출의 결과로 넘어올 수도 있고,
  • JNI 호출로부터 넘어올 수도 이고,
  • 자바 호출로부터 넘어올 수도 있다.

여기서 가장 마지막 경우는 단순히 앞의 세가지 경우의 마지막에 속할뿐이다.
VM이 예외가 던졌다는 것을 알아차렸을때, 해당예외를 처리하는 가장가까운 핸들러를 찾기 위해 HotSPotVM 런타임시스템이 수행된다. 이 때, 핸들러를 찾기 위해서 다음 3개의 정보가 사용된다.

  • 현재 메서드
  • 현재 바이트 코드
  • 예외 객체

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

댓글