본문 바로가기

Java/자바 성능 개선

16. JVM은 도대체 어떻게 구동될까?

💡 본 게시글은 이상민 저자의 '자바 성능 튜닝 이야기' 교재를 공부하고, 이에 대해 정리한 내용입니다.

들어가며

 전 세계의 대부분 시스템은 지속적으로 변화하고 있습니다. 즉, 시스템을 주기적으로 수정하고 배포하는 작업을 반복하게 됩니다. 웹 기반 시스템을 배포할 때 재시작만 수행한다면, 사용자는 배포 직후에 느린 응답 시간을 경험하게 될 수 있습니다. 이로 인해 사용자는 시스템에 대한 불만이 증가하게 될 수 있습니다. 이를 방지하기 위해 '워밍업'이 필요합니다. 그렇다면 왜 워밍업이 필요한지, 그 중요성에 대해 알아보겠습니다.

  • HotSpot VM의 구조와 워밍업
  • JIT 옵티마이저
  • JVM의 구동 절차
  • JVM의 종료 절차
  • 클래스 로딩의 절차
  • 예외 처리의 절차

 이러한 과정들은 Java의 HotSpot VM에서 핵심적인 역할을 합니다. 이러한 과정들을 이해하고, 그에 따른 최적화를 수행하는 것이 워밍업의 일부입니다.

 

http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

 

HotSpot Glossary of Terms

HotSpot Glossary of Terms A work in progress, especially as the HotSpot VM evolves. But a place to put definitions of things so we only have to define them once. There are empty entries (marked TBD for "to be defined") because we think of things that we ne

openjdk.org

 더욱 자세한 정보를 원한다면, OpenJDK에서 제공하는 HotSpot 관련 용어 해설 페이지를 참조하시면 좋습니다. 이 페이지에서는 HotSpot에 대한 깊은 이해를 돕기 위해 다양한 용어와 개념들을 자세히 설명하고 있습니다.


1) HotSpot VM의 구성과 작동 원리

 자바 관련 문서를 읽다 보면 'HotSpot VM'이라는 용어를 자주 접하게 됩니다. 이를 이해하기 위해서는 먼저 'HotSpot'이 무엇인지부터 알아봐야 합니다.

 

http://en.wikipedia.org/wiki/HotSpot

 

Hotspot - Wikipedia

From Wikipedia, the free encyclopedia Look up hot spot in Wiktionary, the free dictionary. Hotspot, Hot Spot or Hot spot may refer to: Arts, entertainment, and media[edit] Fictional entities[edit] Other uses in arts, entertainment, and media[edit] Computin

en.wikipedia.org

 

 'HotSpot'은 원래 '뜨거운 지점'이라는 뜻으로, 일반적으로 '분쟁 지역'이나 '활기 넘치는 곳' 등의 의미로 사용됩니다. 하지만 자바에서는 'Java HotSpot Performance Engine'이라는 이름으로, 특정 부분의 성능을 최적화하는 엔진을 가리킵니다. 그렇다면 왜 이런 이름이 붙었을까요?

 

 자바를 개발한 Sun에서는 자바의 성능을 향상시키기 위해 Just In Time(JIT) 컴파일러를 만들었고, 그를 'HotSpot'이라 명명했습니다. JIT 컴파일러프로그램의 성능에 큰 영향을 미치는 부분을 지속적으로 분석하며, 분석된 부분은 성능 향상을 위한 최적화의 대상이 됩니다.

 

 이 HotSpot은 Java 1.3 버전부터 기본 VM으로 사용되어 왔으며, 현재 운영 중인 대부분의 시스템에서 사용되는 VM입니다. HotSpot VM은 크게 세 가지 주요 컴포넌트로 구성되어 있습니다. 

  • VM(Virtual Machine) 런타임 
  • JIT(Just In Time) 컴파일러
  • 메모리 관리자

 HotSpot VM은 높은 성능확장성을 제공합니다. 특히 JIT 컴파일러는 자바 애플리케이션이 수행되는 상황에 따라 동적으로 최적화를 수행하여, 더욱 효율적인 실행을 가능하게 합니다.

JIT는 '적절한 시간'이라는 의미의 우리말로 해석될 수 있습니다.
JIT를 사용한다는 것은 '자바 메서드가 호출될 때마다 해당 바이트 코드를 컴파일해 실행 가능한 네이티브 코드로 변환한다'는 뜻입니다. 그러나, 모든 경우에 JIT 컴파일을 수행하면 성능 저하가 발생할 수 있기 때문에, 이를 방지하기 위해 최적화 단계를 거치게 됩니다.

 

※ HotSpot VM의 아키텍처 그림

 

 HotSpot VM의 아키텍처를 살펴보면, 'HotSpot VM 런타임'에 'GC 방식'과 'JIT 컴파일러'를 레고 블록처럼 결합하여 사용할 수 있음을 알 수 있습니다. 이를 가능하게 하기 위해 'VM 런타임'은 JIT 컴파일러와 가비지 컬렉터를 위한 API를 제공합니다. 또한 JVM을 시작하는 런처, 스레드 관리, JNI 등의 기능도 VM 런타임에서 제공합니다.


2) JIT Optimizer라는 게 도대체 뭘까?

 HotSpot VM JIT 컴파일러에 대해 자세히 살펴보기 전에, 이 컴파일러가 Client 버전 Server 버전으로 나뉜다는 사실을 기억해야 합니다.

 

 '컴파일'이란 상위 레벨의 언어로 작성된 코드를 기계에 의존적인 코드로 변환하는 작업을 가리킵니다.

 

예를 들어, C나 C++ 같은 전통적인 컴파일러를 생각해보면, C 언어는 먼저 소스코드에서 오브젝트 파일을 생성하고, 이를 이용해 실행 가능한 라이브러리를 만듭니다. 이 작업은 애플리케이션이 실행되는 동안 지속적으로 반복되는 것이 아니라 한 번만 수행됩니다.

 

하지만 자바는 'javac'라는 컴파일러를 사용하며, 이 컴파일러는 소스코드를 바이트 코드로 변환된 '.class' 파일로 만듭니다. 따라서 JVM은 항상 바이트 코드로 시작해 동적으로 기계에 의존적인 코드로 변환하게 됩니다.

 

 JIT는 애플리케이션에서 모든 메서드를 컴파일 할 수 있는 시간적 여유가 충분하지 않습니다. 따라서 모든 코드는 처음에는 인터프리터에 의해 실행되며, 해당 코드가 충분히 많이 사용되면 컴파일의 대상이 됩니다.

 

 HotSpot VM에서 이 과정은 메서드 내의 카운터를 통해 제어되며, 각 메서드에는 두 가지 카운터가 있습니다.

  • 수행 카운터(Invocation Counter) : 메서드가 시작될 때마다 증가합니다.
  • 백에지 카운터(Backedge Counter) : 높은 바이트 코드 인덱스에서 낮은 인덱스로 컨트롤 흐름이 변경될 때마다 증가합니다.

 여기서 백에지 카운터는 메서드 내에 루프가 존재하는지 확인할 때 사용되며, 수행 카운터보다 컴파일 우선순위가 높습니다.

 

 이 카운터들이 인터프리터에 의해 이 카운터들이 증가될 때마다, 그 값들이 설정한 한계치에 도달했는지 확인합니다. 한계치에 도달하면 인터프리터는 컴파일을 요청합니다. 여기서 수행 카운터의 한계치는 'CompileThreshold'를, 백에지 카운터의 한계치는 다음의 공식을 통해 계산합니다.

 CompileThreshold * OnStackReplacePercentage / 100

 

 

 이 두 가지 값은 JVM이 시작될 때 지정할 수 있으며, 시작 옵션을 통해 다음과 같이 설정할 수 있습니다.

  • XX:CompileThreshold=35000
  • XX:OnStackReplacePercentage=80

 즉, 이렇게 설정하면, 메서드가 3만 5천 번 호출될 때 JIT에서 컴파일을 수행합니다. 또한, 백에지 카운터가 '35,000 * 80 / 100 = 28,000'에 도달했을 때 컴파일이 이루어집니다.

컴파일 요청이 발생하면, 해당 요청은 컴파일 대상 목록의 큐에 추가됩니다. 컴파일러 스레드가 이 큐를 모니터링하며, 스레드가 휴식 상태일 때 큐에서 대상을 꺼내 컴파일을 시작합니다.

 

일반적으로 인터프리터는 컴파일이 완료되기를 기다리지 않고, 수행 카운터를 초기화한 후 메서드 실행을 계속합니다. 컴파일이 완료되면, 컴파일된 코드와 메서드가 연결되고, 그 이후로는 메서드 호출 시 컴파일된 코드를 사용하게 됩니다.

 

인터프리터가 컴파일 완료를 기다리도록 하려면 JVM 시작 시 '-Xbatch' 또는 '-XX:BackgroudCompilation' 옵션을 지정하여 컴파일을 대기하도록 할 수 있습니다.

 

HotSpot VM은 OSR(On Stack Replacement)라는 특별한 컴파일도 수행합니다. 이 OSR은 인터프리터가 실행한 코드 중에서 오랫동안 반복되는 경우에 활용됩니다. 만약 해당 코드가 컴파일되어 있지만 최적화가 이루어지지 않는 코드가 실행 중이라면, 인터프리터 상태를 유지하지 않고 컴파일된 코드로 전환합니다. 이 과정은 오랫동안 반복되는 루프가 다시 호출되지 않을 경우에는 크게 도움이 되지 않지만, 루프가 끝나지 않고 계속 실행되는 경우에는 큰 도움이 됩니다.

 

 Java 5 HotSpot VM이 출시됨에 따라 새로운 기능이 추가되었습니다. 이 기능은 JVM이 시작할 때 플랫폼과 시스템 설정을 평가하여 자동으로 가비지 컬렉터를 설정하고, 자바 힙 크기와 JIT 컴파일러를 선택합니다. 이를 통해 애플리케이션의 활동 및 객체 할당 비율에 따라 가비지 컬렉터가 자바 힙 크기를 동적으로 조절하며, 'New'의 'Eden'과 'Survivor', 'Old' 영역의 비율도 자동으로 조절합니다. 이 기능은 '-XX:+UseParallelGC'와 '-XX:+UseParallelOldGC'에서만 적용되고, '-XX:-UseAdaptiveSizePolicy' 옵션을 적용하여 이 기능을 비활성화할 수 있습니다.

3) JRockit의 JIT 컴파일 및 최적화 절차

 아쉽게도 Oracle JVM의 경우 JIT 컴파일러 최적화에 대한 자세한 문서가 존재하지 않습니다. 그런데, JRockit JVM과 IBM의 경우 문서화된 JIT 최적화 내용이 있으니 한 번 살펴 보겠습니다.

 

먼저 밑의 그림을 보겠습니다.

 

 JVM은 각 OS에서 작동할 수 있도록 자바 코드를 입력 값(정확하게는 바이트 코드)으로 받아 각종 변환을 거친 후 해당 칩의 아키텍처에서 잘 돌아가는 기계어 코드로 변환되어 수행되는 구조로 되어 있습다. 보다 상세한 최적화 절차는 다음의 그림을 보겠습니다.

 

JRockit은 이와 같이 최적화 단계를 거치도록 되어 있으며, 각각의 단계는 다음과 같이 작업을 수행합니다.

 

(1) JRockit의 JIT 컴파일 실행 자바

  • 애플리케이션을 실행하면 기본적으로 1단계인 JIT 컴파일을 거쳐 실행됩니다. 이 과정을 통과한 후 메서드가 수행되면, 그 이후부터는 컴파일된 코드를 호출하게 되어 처리 성능이 향상됩니다. 애플리케이션 시작 시 수천 개의 새 메서드가 수행되는데, 이 때문에 다른 JVM에 비해 JRockit JVM이 초기에는 느릴 수 있습니다. 하지만, JIT가 메서드를 수행하고 컴파일하는 과정은 오버헤드가 발생하더라도, JIT 없이는 JVM이 계속 느린 상태를 유지하게 됩니다. 즉, JIT를 사용하면 시작 시의 성능은 느리지만, 지속적으로 수행할 때는 빠른 처리가 가능합니다. 그래서 모든 메서드를 컴파일하고 최적화하는 과정은 JVM 시작 시간을 느리게 만들기 때문에, 시작부터 모든 메서드를 최적화하진 않습니다.


(2) JRockit의 스레드 모니터링

  • JRockit에는 'sampler thread'라는 스레드가 있어, 주기적으로 애플리케이션의 스레드 상태를 확인합니다. 이 스레드는 어떤 스레드가 작동 중인지, 그리고 그 수행 내역을 관리합니다. 이 정보를 통해 어떤 메서드가 많이 사용되는지 파악하고, 이를 바탕으로 최적화 대상을 선정합니다.

 

(3) JRockit JVM의 최적화 실행

  • 'sampler thread'가 식별한 대상을 최적화합니다. 이 과정은 백그라운드에서 진행되며, 실행 중인 애플리케이션에는 영향을 미치지 않습니다.

간단하게 JRockit JIT의 동작 방식을 간략하게 살펴보았습니다. 실제로 코드가 어떻게 최적화되는지를 보여드리면 이해하기 더 쉬울 것입니다. 아래에는 간단한 A와 B 클래스가 있습니다.

Class A {
    B b;
    public void foo() {
        y = b.get();
        // 중간 생략
        z = b.get();
        sum = y + z;
    }
} 

Class B {
    int value;
    final int get() {
        return value;
    }
}

 

 foo() 메서드를 보면, y와 z 변수에 b.get() 메서드를 호출하고 그 결과를 더합니다. 이때 중복 호출이 발생합니다. 게다가 B 클래스의 get() 메서드는 단순히 value 값을 반환하는 코드입니다. 이 코드는 JRockit JIT 컴파일러에서 다음과 같이 최적화됩니다.

Class A {
    B b;
    publid void foo() {
        y = b.value;
        // 중간 생략
        sum = y + y;
    }
}

 

 B 클래스의 코드는 그대로이며, A 클래스의 foo 메서드는 위와 같이 변환됩니다. 단순해 보이지만, 다음의 절차를 통해 최적화 작업이 수행됩니다.

최적화 단계 코드변환 설명
시작 단계 변경 없음  
1. final로 선언된 메서드 인라인
(inline 처리)
public void foo() {
    y = b.value;
    // 중간 생략
    z = b.value;
    sum = y + z;
}
b.get()이 b.value로 변환된다.
이 작업을 통해서 메서드 호출로 인한 성능 저하가 개선된다.
2. 불필요한 부하 제거 public void foo() {
    y = b.value;
    // 중간 생략
    z = y;
    sum = y +z;
}
z와 y 값이 동일하므로, z에 y 값을 할당한다.
3. 복제 public void foo() {
    y = b.value;
    // 중간 생략
    y = y;
    sum = y + y;
}
z와 y의 값이 동일하므로 불필요한 변수인
z를 y로 변경한다.
4. 죽은 코드 삭제 public void foo() {
    y = b.value;
    // 중간 생략
    sum = y + y;
}
y = y 코드가 불필요하므로 삭제한다.

 아주 간단한 예제지만, 4가지 최적화 기법이 적용된 것을 확인할 수 있습니다.

이 절의 내용은 오라클 홈페이지에서 확인할 수 있습니다.
http://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/underst_jist.html

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

IBM JVM의 JIT 컴파일 방식은 아래의 5가지 단계로 나뉩니다.

  1. 인라이닝(Inlining)
  2. 지역 최적화(Local optimization)
  3. 조건 구문 최적화(Control flow optimization)
  4. 글로벌 최적화(Global optimization)
  5. 네이티브 코드 최적화(Native code generation) 

(1) 인라이닝

  • 메서드가 단순한 경우에 적용되며, 호출된 메서드가 간단하다면 그 내용이 호출한 메서드의 코드에 포함되게 됩니다. 이로 인해 자주 호출되는 메서드의 성능이 향상됩니다.

(2) 지역 최적화

  • 소규모 코드 블록을 분석하고 개선하는 작업을 수행합니다.

(3) 조건 구문 최적화

  • 메서드 내의 조건 구문을 최적화하며, 효율성을 위해 코드의 실행 경로를 변경합니다.

 

(4) 글로벌 최적화

  • 메서드 전체를 최적화하는 방식으로, 컴파일 시간이 많이 소요되지만, 성능 개선이 크게 이루어질 수 있습니다.

 

(5) 네이티브 코드 최적화

  • 이 방식은 플랫폼 아키텍처에 의존적입니다. 즉, 아키텍처에 따라 최적화 방법이 달라집니다.

 컴파일된 코드는 '코드 캐시(Code cache)'라는 JVM 프로세스 영역에 저장됩니다. 따라서 JVM 프로세스는 JVM 실행 파일과 컴파일된 JIT 코드의 집합으로 구성됩니다.

 

이 내용은 IBM 리눅스용 JDK 문서를 참조하여 작성하였습니다. http://publib.boulder.ibm.com/infocenter/java7sdk/v7r0/topic/com.ibm.java.Inx.70.doc/diag/understanding/jit.html

5) JVM이 시작할 때의 절차

※ HelloWorld라는 클래스를 java 명령으로 실행하면 어떤 단계로 진행될까요? 

1) java 명령줄 옵션 파싱 :

  • 일부 명령들은 적절한 JIT 컴파일러 선택 등을 위해 자바 실행 프로그램에서 사용되며, 나머지는 HotSpot VM에 전달됩니다.

 

2) 자바 힙 크기 할당 및 JIT 컴파일러 타입 지정 :

  • 메모리 크기나 JIT 컴파일러 종류가 지정되지 않았다면, 자바 실행 프로그램이 시스템 상황에 맞게 선택합니다. 이 과정은 복잡한 HotSpot VM Adaptive Tuning을 거칩니다.

 

3) 환경 변수 지정 :

  • CLASSPATH와 LD_LIBRARY_PATH 등의 환경 변수를 설정합니다.

 

4) Main 클래스 확인 :

  • 자바의 Main 클래스가 지정되지 않았다면, jar 파일의 manifest 파일에서 Main 클래스를 확인합니다.

 

5) HotSpot VM 생성 :

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

 

6) 속성 정보 읽기 :

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

 

7) main() 메서드 수행 :

  • CallStaticVoidMethod를 통해 네이티브 인터페이스를 호출하며, HotSpot VM에 있는 main() 메서드가 수행됩니다. 이때 자바 실행 시 Main 클래스 뒤에 있는 값들이 전달됩니다.

 

※ 자바의 가상 머신(JVM)을 생성하는 JNI_CreateJavaVM 단계에 대해 더 자세히 살펴보겠습니다. 

1) JNI_CreateJavaVM 호출 제한 :

  • 동시에 두 개의 스레드에서 호출할 수 없으며, 프로세스 내에서 오직 하나의 HotSpot VM 인스턴스만 생성될 수 있도록 보장됩니다.

 

2) JNI 버전 호환성 점검 및 GC 로깅 준비 :

  • JNI 버전의 호환성을 점검하고, GC 로깅을 위한 준비를 마칩니다.

 

3) OS 모듈 초기화 :

  • 랜덤 번호 생성기, PID 할당 등의 OS 모듈들이 초기화됩니다.

 

4) 커맨드 라인 변수와 속성 전달 :

  • 커맨드 라인 변수와 속성들이 JNI_CreateJavaVM 변수에 전달되며, 이후 사용을 위해 파싱하고 보관합니다.

 

5) 표준 자바 시스템 속성 초기화 :

  • 표준 자바 시스템 속성(properties)이 초기화됩니다.

 

6) 모듈 초기화 :

  • 동기화, 메모리, safepoint 페이지 등의 모듈들이 초기화됩니다.

 

7) 라이브러리 로드 :

  • libzip, libhpi, libjava, libthread 등의 라이브러리들이 로드됩니다.

 

8) 시그널 처리기 초기화 및 설정 :

  • 시그널 처리기가 초기화되고 설정됩니다.

 

9) 스레드 라이브러리 초기화 :

  • 스레드 라이브러리가 초기화됩니다.

 

10) 출력 스트림 로거 초기화 :

  • 출력(output) 스트림 로거가 초기화됩니다.

 

11) 에이전트 라이브러리 초기화 및 시작 :

  • JVM을 모니터링하기 위한 에이전트 라이브러리가 설정되어 있다면, 초기화하고 시작합니다.

 

12) 스레드 상태와 스레드 로컬 저장소 초기화 :

  • 스레드 처리를 위해 필요한 스레드 상태와 스레드 로컬 저장소를 초기화합니다.

 

13) '글로벌 데이터' 초기화 :

  • HotSpot VM의 '글로벌 데이터'들이 초기화됩니다. 글로벌 데이터에는 이벤트 로그(event log), OS 동기화, 성능 통계 메모리(perfMemory), 메모리 할당자(chunkPool)들이 포함됩니다.

 

14) 스레드 생성 가능 상태 :

  • HotSpotVM에서 스레드를 생성할 수 있는 상태가 됩니다. Main 스레드가 생성되고, 현재 OS 스레드에 연결됩니다. 하지만 아직 스레드 목록에 추가되지 않습니다.

 

15) 자바 레벨 동기화 초기화 및 활성화 :

  • 자바 레벨의 동기화가 초기화되고 활성화됩니다.

 

16) 부트 클래스로더, 코드 캐시, 인터프리터, JIT 컴파일러, JNI, 시스템 dictionary, '글로벌 데이터' 구조의 집합인 universe 등 초기화 :

  • 부트 클래스로더, 코드 캐시, 인터프리터, JIT 컴파일러, JNI, 시스템 dictionary, '글로벌 데이터' 구조의 집합인 universe 등이 초기화됩니다.

 

17) 자바 main 스레드 추가 및 상태 점검 :

  • 스레드 목록에 자바 main 스레드가 추가되며, universe의 상태를 점검합니다. 이 때, 중요한 기능을 하는 HotSpot VMThread가 생성됩니다. 이 시점에서 HotSpot VM의 현재 상태를 JVMTI에 전달합니다.

 

18) 클래스 로딩 및 초기화 :

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

 

19) HotSpot VM의 기능 시작 :

  • HotSpot VM의 시그널 핸들러 스레드가 시작되고, JIT 컴파일러가 초기화되며, HotSpot의 컴파일 브로커 스레드가 시작됩니다. 이후, HotSpot VM과 관련된 다양한 스레드들이 시작됩니다. 이 시점부터 HotSpot VM의 전체 기능이 작동합니다.

 

20) JNIEnv 시작 :

  • JNIEnv가 시작되며, HotSpot VM을 시작한 호출자에게 새로운 JNI 요청을 처리할 준비가 되었다고 알립니다.

이렇게 복잡한 JNI_CreateJavaVM 시작 단계를 거친 후, 나머지 단계들을 수행하면 JVM이 시작됩니다.


6) JVM 종료 절차

 그러면 JVM이 종료되는 과정은 어떤 단계를 거치는지 알아보겠습니다. JVM은 정상적으로 종료되는 경우에만 이 절차를 따르며, OS의 'kill -9'와 같은 명령으로 강제로 종료될 경우에는 이 절차를 따르지 않습니다.

 

만약 JVM 시작 중에 오류가 발생하여 시작을 중지하거나, JVM에 심각한 에러가 발생하여 중지해야 할 필요가 있을 때, HotSpot 런처는 'DestroyJavaVM' 메서드를 호출합니다.

 

※ HotSpot VM의 종료는 'DestroyJavaVM' 메서드의 종료 절차를 따릅니다. 

1) HotSpot VM이 작동 중일 때, 데몬이 아닌 스레드(non-daemon thread)가 모두 수행될 때까지 대기합니다.

 

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 종료를 요청한 호출자에게 제어를 반환합니다.

 

이렇게 다양한 단계를 거쳐 JVM이 안전하게 종료됩니다.


7) 클래스 로딩 절차

※ 자바의 클래스가 어떻게 메모리에 로딩되는지에 대한 절차를 살펴봅시다.

  1. 주어진 클래스 이름에 해당하는 바이너리 형식의 자바 클래스를 클래스 패스에서 찾습니다.
  2. 찾은 자바 클래스를 정의합니다.
  3. java.lang 패키지의 Class 클래스 객체를 생성하여 해당 클래스를 나타냅니다.
  4. 그다음, 링크 작업을 수행합니다. 이 단계에서는 static 필드를 생성하고 초기화하며, 메서드 테이블을 할당합니다.
  5. 마지막으로, 클래스의 초기화를 진행합니다. 이 과정에서 클래스의 static 블록과 static 필드가 우선적으로 초기화되며, 부모 클래스의 초기화가 해당 클래스의 초기화 전에 이루어집니다.

이러한 단계들이 복잡해 보일 수 있지만, 기억해야 하는 키워드는 loading -> linking -> initializing입니다.

※ 클래스를 로딩할 때 다음의 에러들이 발생할 수 있으나, 이러한 에러들은 일반적으로 자주 발생하지 않습니다.

  • NoClassDefFoundError : 클래스 파일을 찾지 못했을 때 발생합니다.
  • ClassFormatError : 클래스 파일의 포맷이 잘못된 경우에 발생합니다.
  • UnsupportedClassVersionError : 상위 버전의 JDK에서 컴파일한 클래스를 하위 버전의 JDK에서 실행하려고 할 때 발생합니다.
  • ClassCircularityError : 부모 클래스를 로딩하는 과정에서 문제가 발생할 때 나타납니다. 자바는 클래스를 로딩하기 전에 부모 클래스들을 미리 로딩해야 합니다.
  • IncompatibleClassChangeError : 클래스가 부모 클래스인데 implements를 하는 경우나, 인터페이스가 부모인데 extends하는 경우에 발생합니다.
  • VerifyError : 클래스 파일의 semantic, 상수 풀, 타입 등에 문제가 있을 때 발생합니다.

 그런데, 클래스 로더가 클래스를 찾아 로딩하는 과정에서 다른 클래스 로더에 클래스 로딩을 요청하는 경우가 있습니다. 이를 'class loader delegation'이라고 부릅니다. 클래스 로더는 계층적으로 구성되어 있으며, 기본 클래스 로더는 '시스템 클래스 로더'라고 불립니다. 여기에는 main 메서드가 있는 클래스와 클래스 패스에 있는 클래스들이 포함되어 있습니다. 이 클래스 로더 아래에는 애플리케이션 클래스 로더가 있으며, 이는 자바 SE의 기본 라이브러리가 될 수도 있고, 개발자가 임의로 생성한 것일 수도 있습니다.

※ 부트스트랩(Bootstrap) 클래스 로더

 HotSpot VM은 부트스트랩 클래스 로더를 구현하고 있습니다. 이는 HotSpot VM의 BOOTCLASSPATH에서 클래스들을 로드합니다. 예를 들어, Java SE(Standard Edition) 클래스 라이브러리들을 포함하는 rt.jar가 이에 해당합니다.

JVM의 빠른 시작을 위해, 클라이언트 HotSpot VM은 '클래스 데이터 공유'라는 기능을 통해 클래스를 미리 로딩할 수 있습니다. 이 기능은 기본적으로 활성화되어 있으며, -Xshare:on 옵션을 사용해 명시적으로 활성화하거나 -Xshare:off 옵션을 사용해 비활성화할 수 있습니다. 서버 HotSpot VM은 이 기능을 제공하지 않으며, 클라이언트 VM도 시리얼 GC를 사용하지 않는 경우 이 기능을 제공하지 않습니다.


※  HotSpot의 클래스 메타데이터(Metadata)

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

 

※  내부 클래스 로딩 데이터 관리
 HotSpot VM은 클래스 로딩을 추적하기 위해 SystemDictionary, PlaceholderTable, LoaderConstraintTable과 같은 3개의 해시 테이블을 관리합니다.

  • SystemDictionary: 로드된 클래스를 포함하며, 클래스 이름 및 클래스 로더를 키로 가지고 klassOop를 값으로 가집니다. 이는 safepoint에서만 제거됩니다.
  • PlaceholderTable: 현재 로딩된 클래스들에 대한 정보를 관리합니다. 이는 ClassCircularity Error를 체크하거나, 다중 스레드에서 클래스를 로딩하는 클래스 로더에서 사용됩니다.
  • LoaderConstraintTable: 타입 체크 시의 제약 사항을 추정하는 용도로 사용됩니다.

8) JVM에서 예외를 처리하는 과정

 JVM에서 예외 처리 과정을 함께 살펴봅시다.

 

JVM은 자바 언어의 제약 사항을 위반할 경우, 이를 예외(exception)라는 신호로 처리합니다. HotSpot VM의 인터프리터, JIT 컴파일러 그리고 다른 HotSpot VM 컴포넌트들은 모두 예외 처리와 관련이 있습니다.

 

일반적으로 예외 처리는 아래 두 가지 상황에서 발생합니다.

1) 예외를 발생시킨 메서드 내에서 처리하는 경우

2) 호출한 메서드에서 처리하는 경우 두 번째 경우는 보다 복잡한데, 적절한 핸들러를 찾기 위해 스택을 거슬러 올라가는 작업이 필요합니다.

 

예외는 다음과 같은 경우에 발생할 수 있습니다.

  • 예외를 발생시킨 바이트 코드에 의해 초기화될 수 있습니다.
  • VM 내부 호출의 결과로 발생할 수 있습니다.
  • JNI 호출로부터 발생할 수 있습니다.
  • 자바 호출로부터 발생할 수 있습니다.

여기서 마지막 경우는 사실상 앞의 세 가지 경우의 마지막 단계에 속합니다.

 JVM이 예외가 발생했음을 인지하면, HotSpot VM 런타임 시스템이 가장 가까운 핸들러를 찾아 이 예외를 처리합니다. 이 과정에서는 현재 메서드, 현재 바이트 코드, 예외 객체라는 세 가지 정보가 필요합니다.

 

현재 메서드에서 핸들러를 찾지 못하면, 현재 실행되고 있는 스택 프레임을 통해 이전 프레임을 찾는 작업을 수행합니다. 적절한 핸들러를 찾게 되면, HotSpot VM의 실행 상태가 변경되고, 해당 핸들러로 이동하여 자바 코드의 실행이 계속됩니다.