CS/JAVA
Java Virtua Machine
lovelyunsh
2023. 8. 23. 13:24
JVM 동작과정
- 클래스 로더가 자바 바이트 코드를 읽어 런타임 데이터 영역에 로드
- 실행 엔진이 자바 바이트 코드를 실행.
자바 바이트 코드
자바 바이트 코드 OpCode와 피연산자로 이루어짐.
명령어의 크기가 1바이트로 이루어져 바이트 코드라고 부른다.
class loader
동적 로드 : 런타임에서 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크함.
동적 로드를 담당하는 것이 class loader.
- 로딩(loading)
- 바이트 코드를 읽어와 method area에 저장
- 링크(linking)
- 검증(verify) : 바이트 코드가 유효하고 안전한지 확인 (Compile Error)
- 준비(prepare) : static 변수에 사용할 메모리를 할당하고 기본값으로 초기화
- 해결(resolve) : constant pool에 있던 심볼릭 링크를 실제 레퍼런스로 교체 (new, instanceof)
- 초기화(Initialization)
- prepare에서 준비한 메모리에 static 변수의 값을 할당.
클래스 로더의 종류
- 부트스트랩 클래스 로더 (Bootstrap Class Loader)
- 이 클래스 로더는 JVM의 핵심 라이브러리를 JVM 실행과 동시에 로드
- Java 8
- jre/lib/rt.jar 및 기타 핵심 라이브러리와 같은 JDK의 내부 클래스를 로드한다.
- Java 9 이후
- Java Platform Module System (JPMS)이 도입되면서 더 이상 /rt.jar이 존재하지 않으며, /lib 내에 모듈화되어 포함.
- (java.lang, java.util 에 속하는 클래스, 네이티브 라이브러리 등)
- 확장 클래스 로더(Extension Class Loader)
- JAVA_HOME/ext 디렉토리 아래에 있는 확장 클래스를 로드
- 사용자가 설치한 확장 라이브러리들을 로드
- 시스템 클래스 로더(System Class Loader) = 어플리케이션 클래스 로더(Application Class Loader)
- 사용자가 작성한 클래스 및 $CLASSPATH에 있는 클래스를 로드
- 보통 애플리케이션의 시작 지점에 있는 main 메서드와 같은 부분에서 이 클래스 로더가 사용
- 클래스로더 원칙
- 가시성 원칙(Visibility Principle)
- 자식 클래스로더는 부모 클래스로더가 로드한 클래스를 볼 수 있지만, 부모 클래스로더는 자식 클래스로더가 로드한 클래스를 볼 수 없다
- 유일성 원칙(Uniqueness Principle)
- 부모가 로드한 클래스를 자식이 다시 로드 해서는 안된다.
- 위임 계층 원칙(Delegation Hierarchy Principle)
- 가시성 원칙과 유일성 원칙을 지키기 위해 부모 계층 부터 검색을 시작.
- 마지막까지 클래스를 찾지 못하면 java.lang.ClassNotFoundException 에러 발생
- 가시성 원칙(Visibility Principle)
Runtime Data Area
- JVM이 프로그램 수행을 위해 OS로 부터 할당 받은 메모리 영역
- Method Area와 Heap 모든 쓰레드가 공유
- PC Register, Java Stack, Native Method Stack은 쓰레드 단독 사용
1. Method Area
- JVM 시작 시 생성되고 JVM 종료 시 소멸
- Runtime Constant Pool
- 클래스/인터페이스의 메소드, 필드, 문자열 상수 등의 레퍼런스가 저장, 이들의 물리적 메모리 위치를 참조할 경우에 사용.
- Static 변수
- 객체 생성없이 static 변수를 사용할 수 있는 이유.
- 클래스 로더에 의해 읽어온 모든 class 정보
PermGen(JDK 7) -> Metaspace (JDK 8)
- 기존 PermGen 영역이 JDK8 부터 Metaspace로 대체 되었음.
- PermGen영역이 제한적이고 고정된 크기였기에 OutOfMemoryError를 유발
- Metaspace는 OS에서 제공하는 native 메모리를 사용하여, 필요시 자동으로 크기를 증가시켜 공간을 확보. 최대 크기에 도달하면 GC가 돌아 OOM error의 발생확률을 줄임.
2. Heap
- Object 클래스를 상속받는 모든 객체들(클래스 인스턴스들)과 해당 인스턴스의 변수들을 담는 공간
- Heap 영역은 GC를 통해 저장공간을 관리
3. JVM Stack
- Thread가 생성될 때마다 생성되는 공간
- Stack Frame을 저장하는 메모리
- stack 영역이 꽉찰 경우 java.lang.StackOverFlowError를 throw
4. PC Register
- 현재 실행 중인 명령어의 주소를 저장하는 레지스터
- 쓰레드 마다 가지고 있어 각 쓰레드의 명령어 실행 상태를 제어하고 추적하는 데 용이
- 메서드 호출 시 메서드의 첫 번째 명령어 주소를 저장한다.
- 메서드 실행이 완료되고 호출 지점으로 돌아가기 위한 반환 주소도 저장함.
5. 네이티브 메서드 스택
- Java Native Interface에 의해 실행되는 Native 코드를 실행하기 위한 영역
- 자바 스택과 별개로 네이티브 메소드를 위해서 쓰이는 영역
- native 키워드가 붙은 메서드의 실행
JNI Specification
https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
실행 엔진
- 자바 인터프리터
- 바이트 코드를 순차적으로 읽으며 번역하고 실행.
- 같은 메서드를 호출 하더라도 매번 번역을 새로 수행.
- JIT (Just In Time) 컴파일러
- 인터프리터에 의해 여러번 수행된 코드를 Native Code로 변환해서 code cache 영역에 저장됨
- C1 컴파일러와 C2 컴파일러가 있음.
- 호출 횟수가 특정 임계치를 넘을 때 마다 interpreter → c1 → c2 순으로 컴파일 진행
- C1 컴파일러는 바이트 코드를 어셈블리 코드로 변환하고, 이를 네이티브 코드로 컴파일하여 캐싱
- C2 컴파일러는 프로그램을 실행하며 프로파일링 정보를 수집하여, 인라인 최적화(메서드 호출을 줄여 성능을 향상)와 루프 최적화(반복문 성능 향상) 등을 수행
JVM Warm Up
https://www.youtube.com/watch?v=CQi3SS2YspY
- JVM 실행 엔진의 특성 때문에 배포 직후 Latency가 발생
- JVM은 자주 실행 되는 코드를 컴파일하고 캐시한다
- 클래스는 필요할 때 Lazy Loading 으로 메모리에 적재된다
- 배포 직후에는 클래스가 호출된 적이 없으니 오래 걸린다.
- 이 때 대용량 트래픽이 몰리면 Latency가 발생
- 애플리케이션이 기동되는 시점에, 자주 호출될 것으로 예상되는 지점의 코드를 충분히 많이 실행하여 배포 직후의 Latency를 해결하자. ← Kubernetes Readiness Probe 활용