JVM을 좀 더 깊게 학습하기 위해서 포스팅했습니다.
(Medium에 엔지니어분께서 작성하신 글을 번역한 글로 보시면 됩니다.)
자바가 내부에서 실제로 작동하는 방식을 알아보려고 합니다.
JVM 아키텍처에 대해서 얘기해보겠습니다.
🟩 01. 클래스 로더 서브시스템 (Class Loader Subsystem)
JVM은 RAM에 띄어져 있습니다.
자바의 동적 클래스 로딩 기능을 이용해서 런타임 중에 클래스 로더 서브시스템을 통해 클래스 파일을 RAM에 띄웁니다.
런타임에 클래스를 처음 참조할 때 클래스 파일(.class)을 로딩, 연결, 초기화 합니다.
🟧 01-01. 로딩 (Loading)
컴파일된 클래스(.class)를 메모리에 로드하는 것은 클래스 로더의 주요 역할입니다.
클래스 로딩 프로세스는 메인 클래스를 로딩하는 것부터 시작합니다.
후속 클래스 로딩을 시도할 경우 이미 실행 중인 클래스의 클래스 참조에 따라 수행됩니다.
- 바이트코드가 클래스에 대한 정적 참조를 만들 경우
- 바이트코드가 클래스 객체를 생성할 경우
클래스 로더는 3가지 유형과 4가지 원칙이 있습니다.
01-01-01. 가시성 원칙
하위 클래스 로더는 상위 클래스 로더가 로드한 클래스를 볼 수 있습니다.
상위 클래스 로더는 하위 클래스 로더가 로드한 클래스를 찾을 수 없습니다.
01-01-02. 유일성 원칙
상위에서 로드한 클래스를 하위 클래스 로더에서 다시 로드하지 않아야 합니다.
즉, 클래스 로드가 중복으로 발생하지 않아야 합니다.
01-01-03. 위임 계층 구조 원칙
JVM은 "가시성 원칙", "유일성 원칙"을 지키기 위해서 클래스 로딩 요청에 대한 클래스 로더를 선택하는 위임 계층을 따릅니다.
Application Class Loader는 하위 수준부터 시작해서 수신한 클래스 로딩 요청을 Extension Class Loader에 위임합니다.
Extension Class Loader는 요청을 Bootstrap Class Loader에 위임합니다.
Bootstrap 경로에 요청된 클래스가 있으면 해당 클래스가 로드됩니다.
만약 없을 경우 요청이 Extension Class Loader 레벨로 전송되어 확장 경로 혹은 사용자 지정 경로에서 클래스를 찾습니다.
이것조차 안된다면 요청이 Application Class Loader로 돌아와 시스템 클래스 경로에서 클래스를 찾습니다.
Application Class Loader도 요청된 클래스를 로드하지 못하면 RuntimeException이 발생합니다.
- Bootstrap Class Loader
- $JAVA_HOME/jre/lib 디렉터리(java.lang.*, java.util.*, ...)에 있는 핵심 Java API 클래스를 JDK 클래스에서 로드합니다.
- C/C++ 같은 네이티브 언어로 구현되어 있으며 자바의 모든 클래스 로더의 부모 역할을 하고 있습니다.
- Extension Class Loader
- 클래스 로딩 요청을 부모인 Bootstrap에 위임합니다.
- 실패할 경우 확장 경로($JAVA_HOME/jre/lib/ext)의 확장 디렉터리에서 클래스를 로드합니다.
- System/Application Class Loader
- 내부적으로 java.class.path에 매핑된 환경 변수를 사용해서 시스템 클래스 경로에서 특정 클래스를 로드합니다.
01-01-04. 언로딩 원칙은 없다.
클래스 로더는 클래스를 로드할 수 있습니다.
반대로 로드된 클래스를 언로드할 수는 없습니다.
언로드 대신 현재 클래스 로더를 삭제하고 새로운 클래스 로더를 생성할 수 있습니다.
🟧 01-02. 연결 (Linking)
연결은 로드된 클래스, 인터페이스, 슈퍼 클래스, 슈퍼 인터페이스 등 필요에 따라서 해당 유형을 확인하고 속성에 따라 준비합니다.
01-02-01. 확인 (Verify)
.class 파일의 정확성을 확인합니다.
- 자바 언어 사양에 따라서 코드가 올바른지
- JVM 사양에 따라 유효한 컴파일러에서 생성되었는지
클래스 로드 프로세스 중 가장 복잡한 테스트 프로세스로 시간이 가장 오래 걸립니다.
링크를 사용하면 클래스 로딩 프로세스가 느려지지만 바이트코드를 실행할 때 검사를 여러 번 수행할 필요가 없기 때문에 전체 실행이 효율적입니다.
다음과 같은 검사를 수행하고 확인에 실패하면 런타임 오류가 발생합니다.
- 재정의되지 않은 메서드/클래스
- 메서드 접근 제어자 키워드
- 메서드의 매개변수 수와 매개변수 타입
- 바이트코드 스택 조작
- 변수의 올바른 유형
01-02-02. 준비 (Prepare)
메서드 테이블과 같이 JVM에서 사용하는 모든 데이터 구조 및 정적 저장소에 대한 메모리를 할당합니다.
정적 필드가 생성되고 기본값으로 초기화되지만 초기화의 일부일 뿐입니다.
이 단계에서는 초기화 프로그램이나 코드가 실행되지 않습니다.
01-02-03. 해결 (Resolve)
유형의 기호 참조를 직접 참조로 변경합니다.
참조된 엔티티를 찾기 위해 메서드 영역을 검색합니다.
🟧 01-03. 초기화
로드된 클래스, 인터페이스의 초기화 로직이 실행됩니다. (클래스 생성자를 호출한다.)
JVM은 다중 스레드로 동작하기에 적절하게 동기화하여 동시에 동일한 클래스 또는 인터페이스가 초기화하는 것을 방지합니다.
여기까지가 정적 변수(static variables)가 코드에 정의된 값으로 정의되고
정적 블럭(static block)이 실행되는 클래스 로딩의 마지막 단계입니다.
🟩 02. 런타임 데이터 영역 (Runtime Data Area)
런타임 데이터 영역(Runtime Data Area)은 JVM이 OS에서 실행될 때 할당되는 메모리 영역입니다.
클래스 로더 서브시스템(Class Loader Subsystem)은 ".class 파일"을 읽는 것뿐만 아니라 바이너리 데이터를 생성하고 각 클래스에 맞게 개별적으로 메서드 영역에 저장합니다.
모든 .class 파일이 로드되었다면, java.lang 패키지에 정의된대로 힙 메모리의 파일을 나타내는 Class 단 하나의 개체를 만듭니다.
이제 이 Class 개체는 코드에서 클래스 수준 정보(클래스 이름, 부모 이름, 메서드, 변수 정보, 정적 변수, ...)를 읽을 수 있습니다.
🟧 02-01. 메서드 영역 (Method Area)
메서드 영역은 JVM 당 하나만 할당되는 공유 자원입니다
모든 JVM 스레드는 메서드 영역을 공유하므로 메서드 데이터, 동적 연결 프로세스는 스레드를 안전하게 관리해야 합니다.
메서드 영역은 클래스 수준 데이터를 저장하고 있습니다.
클래스 로더 참조 | |
런타임 상수 풀 | 숫자 상수, 필드 참조, 메서드 참조, 속성 각 클래스 및 인터페이스 상수, 메서드 및 필드에 대한 모든 참조를 포함하고 있습니다. 메서드 또는 필드가 참조되면 JVM은 런타임 상수 풀을 사용하여 메모리에서 메서드 또는 필드의 실제 주소를 검색합니다. |
필드 데이터 | 필드당: 이름, 타입, 한정자, 속성 |
메서드 데이터 | 메서드 데이터 - 메서드별: 이름, 반환 유형, 매개변수 타입, 한정자, 속성 |
메서드 코드 | 메서드별: 바이트 코드, 피연산자 스택 크기, 지역 변수 크기, 지역 변수 테이블, 예외 테이블 예외 처리: 시작 지점, 끝 지점, 처리하는 코드에 대한 오프셋, catch 되는 예외 클래스에 대한 상수 풀 인덱스 |
🟧 02-02. 힙 영역 (Heap Area)
힙 영역도 JVM 당 하나만 할당되는 공유 리소스입니다.
모든 객체의 정보와 해당 인스턴스 변수, 배열은 힙 영역에 저장됩니다.
힙 영역 또한 여러 스레드에 공유하므로 스레드 안전하게 관리해야 합니다.
이런 힙 엽역은 GC 대상입니다.
🟧 02-03. 스택 영역 (Stack Area)
스택 영역은 공유 리소스가 아닌 JVM 스레드에 대해서 스레드가 시작되면 각 메서드 호출을 저장하기 위해서 별도로 스택이 생성됩니다.
메서드 호출에 대해서 하나의 항목이 생성되고 런타임 스택의 맨 위에 추가됩니다.
이런 항목을 스택 프레임(Stack Frame)이라고도 부릅니다.
각 스택 프레임에는 실행 중인 메서드가 속한 클래스의 지역 변수 배열, 피연산자 스택 및 런타임 상수 풀에 대한 참조 정보가 있습니다.
스택 프레임의 크기는 고정적인데 이유는 지역 변수 배열과 피연산자 스택의 크기는 컴파일 시기에 결정되기 때문입니다.
메서드가 정상적으로 반환되거나 메서드 호출 중에 예외가 발생했다면 스택 프레임에서 제거됩니다.
예외가 발생하면 스택 추척(printStackTrace())을 이용할 수 있습니다.
이러한 스택 영역은 공유 리소스가 아니므로 스레드-세이프 합니다.
02-03-01. 지역 변수 배열
인덱스 0은 메서드가 속한 클래스 인스턴스의 참조입니다.
인덱스 1부터는 메서드로 전송된 매개변수가 저장됩니다.
메서드 매개변수 다음에 메서드 지역 변수가 저장됩니다.
02-03-02. 피연산자 스택
중간 작업을 수행하는 런타임 작업 공간 역할입니다.
각 메서드는 피연산자 스택과 지역 변수 배열간에 데이터를 교환하고 다른 메서드 호출 결과를 추가/제거 합니다.
피연산자 스택 공간이 필요한 크기는 컴파일 시기에 결정될 수 있습니다.
02-03-03. 프레임 데이터
메서드에 연관된 뜻을 여기에 저장합니다.
예외의 경우 catch 블럭의 정보도 프레임 데이터에 저장됩니다.
런타임 스택 프레임은 스레드가 종료된 후 해당 스택 프레임도 JVM에 의해 제거됩니다.
스택은 동적 또는 고정 크기일 수 있습니다.
스레드에 허용된 크기보다 더 큰 스택이 필요한 경우 StackOverFlowError가 발생합니다.
스레드에 할당할 메모리가 충분하지 않을 경우 OutOfMemoryError가 발생합니다.
🟧 02-04. PC 레지스터 (PC Register)
각 JVM 스레드에 대해 스레드가 시작되면 현재 실행 중인 명령의 주소(메서드 영역의 메모리 주소)를 유지하기 위해 별도의 PC 레지스터가 생성됩니다.
🟧 02-05. 네이티브 메서드 스택 (Native Method Stack)
자바 스레드와 기본 운영 체제 스레드 간에 직접 매핑이 있습니다.
자바 스레드는 모든 상태를 준비한 후 JNI를 통해 호출되는 네이티브 메서드 정보를 저장하기 위해 별도의 네이티브 스택을 생성합니다.
한 번 네이티브 스레드가 생성, 초기화 되고 나면 run() 메서드를 호출합니다.
run() 메서드가 반환되면 포착되지 않은 에외가 있는 경우 예외를 처리하고 네이티브 스레드는 스레드 종료의 결과고 JVM을 종료하는지 여부를 확인합니다. 스레드가 종료되면 모든 네이티브 스레드와 자바 스레드 모두 대한 모든 리소스가 해제됩니다.
🟩 03. 실행 엔진 (Execution Engine)
실제 바이트코드 실행이 발생하는 곳입니다.
실행 엔진은 런타임 데이터 영역에 할당된 데이터를 읽어 바이트코드 명령을 한 줄씩 실행시킵니다.
🟧 03-01. 인터프리터 (Interpreter)
인터프리터는 바이트코드를 해석하고 명령을 한 줄씩 실행합니다.
인터프리터는 한 바이트 코드 라인을 빠르게 해석할 수 있지만 메서드를 여러 번 호출할 때마다 새롭게 해석해야 하므로 실행이 느리다는 단점이 있습니다.
🟧 03-02. JIT Compiler
인터프리터만 사용한다면 하나의 메서드를 호출할 때마다 해석하는 중복 작업이 발생합니다.
JIT 컴파일러를 이용하면 이러한 문제를 해결할 수 있습니다.
먼저, 모든 바이트코드를 네이티브 코드(기계어)로 컴파일합니다.
이제 반복되는 메서드 호출에 대해 각각의 명령어를 해석하는 것보다 훨씬 빠른 네이티브 코드를 실행할 수 있도록 제공합니다.
네이티브 코드는 캐시에 저장되므로 컴파일된 코드를 빠르게 실행될 수 있습니다.
다만 JIT 컴파일러를 이용했을 때 인터프리터가 해석하는 것보다 컴파일하는 것이 더 오래 걸립니다.
한 번만 실행되는 코드 세그먼트(code segment)같은 경우 컴파일보다 해석하는 것이 좋습니다.
또한 네이티브 코드는 값비싼 리소스인 캐시에 저장되므로 JIT 컴파일러는 내부적으로 각 메서드 호출 빈도를 확인하고 선택한 메서드가 일정 횟수 이상 발생한 경우에만 각 메서드를 컴파일하도록 결정합니다.
실행 엔진은 JVM 공급 업체의 성능 최적화를 도입할 때 핵심 하위 시스템이 될 수 있습니다.
아래와 같은 구성 요소를 통해 성능을 향상시킬 수 있습니다.
- Intermediate Code Generator는 중간 코드를 생성한다.
- Code Optimizer는 위레서 생성된 중간 코드를 최적화한다.
- Target Code Generator는 네이티브 코드 생성을 담당한다.
- Profiler는 하나의 메서드가 여러 번 호출되는 인스턴스 같은 성능 병목 현상을 찾고 핫스팟(hotspot)이라고도 합니다.
03-02-01. Oracle Hotspot VM
Hotspot VM은 Oracle에서 개발한 JVM 구현체입니다.
현재는 OpenJDK 프로젝트의 일부로 관리되고 있고 자바 애플리케이션을 실행하기 위한 실행 환경을 제공합니다.
Hotspot VM은 성능과 최적화에 중점을 둔 JVM 구현체로, JIT 컴파일러와 동적 프로파일링(Dynamic Profiling) 기술을 사용하여 자바 애플리케이션 실행 속도를 향상시킵니다. 프로파일링(profiling)을 통해서 JIT 컴파일이 가장 필요한 핫스팟을 식별하여 코드의 성능에 중요한 부분을 네이티브 코드로 컴파일 할 수 있습니다.
시간이 지나 컴파일된 메서드가 자주 호출되지 않으면 해당 메서드를 더 이상 핫스팟으로 보지않고 캐시에서 네이티브 코드를 제거하고 인터프리터 모드로 실행하게 합니다. 이러한 방법으로 불필요한 컴파일을 피하고 성능을 향상시킬 수 있습니다.
🟧 03-03 가비지 컬렉터 (Garbage Collector)
JVM은 개체가 참조되고 있다면 개체가 살아있는 것으로 판단합니다.
객체가 더 이상 참조되지 않아 애플리케이션 코드에서 접근하지 못할 때 GC가 해당 개체를 제거하고 사용하지 않는 메모리도 회수합니다.
🟩 04. 자바 네이티브 인터페이스 (Java Native Interface)
자바 네이티브 인터페이스는 실행에 필요한 네이티브 메서드 라이브러리와 상호 작용합니다.
네이티브 라이브러리의 기능을 제공하기 위해 사용되어지며 이를 통해서 JVM은 하드웨어에 고유한 C/C++ 라이브러리에서 호출할 수 있습니다.
🟩 05. 네이티브 메서드 라이브러리
실행 엔진에 필요한 C/C++ 네이티브 라이브러리 모음입니다.
🔗 출처
'💬 언어' 카테고리의 다른 글
동일성과 동등성의 차이 (0) | 2023.03.20 |
---|---|
equals 와 hashcode 를 함께 정의해야 하는 이유는? (0) | 2023.03.20 |