CS/JAVA

Java Virtua Machine

lovelyunsh 2023. 8. 23. 13:24

JVM 동작과정

  1. 클래스 로더가 자바 바이트 코드를 읽어 런타임 데이터 영역에 로드
  2. 실행 엔진이 자바 바이트 코드를 실행.

자바 바이트 코드

자바 바이트 코드 OpCode와 피연산자로 이루어짐.

명령어의 크기가 1바이트로 이루어져 바이트 코드라고 부른다.

class loader

 

동적 로드 : 런타임에서 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크함.

동적 로드를 담당하는 것이 class loader.

  1. 로딩(loading)
    1. 바이트 코드를 읽어와 method area에 저장
  2. 링크(linking)
    1. 검증(verify) : 바이트 코드가 유효하고 안전한지 확인 (Compile Error)
    2. 준비(prepare) : static 변수에 사용할 메모리를 할당하고 기본값으로 초기화
    3. 해결(resolve) : constant pool에 있던 심볼릭 링크를 실제 레퍼런스로 교체 (new, instanceof)
  3. 초기화(Initialization)
    1. 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 에러 발생

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 활용