자바

Java Virtual Machine (JVM)

규동 2022. 10. 6. 23:44
  • 자바 바이트 코드는 JRE 위에서 동작
  • JRE는 자바 API와 JVM으로 구성되며, JVM의 역할은 자바 애플리케이션을 클래스 로더(Class Loader)를 통해 읽어 들여서 자바 API와 함께 실행하는 것

JDK vs JRE

  • JDK : Java Development Kit
  • JRE : Java Runtime Environment
  • JDK는 JRE를 가지고 있고, 컴파일러도 가지고 있다.
  • JDK 컴파일러로 컴파일(.java) -> ByteCode 변환(.class)

가상 머신

  • 자바 바이트코드를 실행하고자 하는 모든 하드웨어에 JVM을 동작시킴으로써 자바 실행 코드를 변경하지 않고 모든 종류의 OS, 하드웨어에서 동작되게 할 수 있다.

JVM 특징

  • 스택 기반: 인텔 x86 아키텍처나 ARM 아키텍처와 같은 하드웨어가 레지스터 기반으로 동작하는 데 비해 JVM은 스택 기반
  • 심볼릭 레퍼런스: Primitive type을 제외한 모든 타입을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
  • 가비지 컬렉션: 클래스 인스턴스는 사용자 코드의 이해 명시적으로 생고 가비지 컬렉션에 의해 자동으로 파괴된다.

JVM구조

https://d2.naver.com/helloworld/1230

클래스 로더(Class Loader)가 컴파일된 자바 바이트코드를 런타임 데이터 영억(Runtime Data Areas)에 로드하고, 실행 엔진(Execution Engine)이 자바 바이트코드를 실행한다.

클래스 로더

자바는 컴파일 타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM 클래스 로더이다.

  • 계층 구조 : 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생선된다.
  • 위임 모델 : 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다.
  • 가시성 제한 : 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
  • 언로드 불가 : 클래스 로더는 클래스를 로드할수는 있지만 언로드할 수는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.

클래스로더

클래스 로드를 요청받으면, 클래스 로더 캐시 -> 상위 클래스 로더 -> 자기 자신의 순서로 해당 클래스가 있는지 확인한다.

  • 부트스트랩 클래스 로더 : JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
  • 익스텐션 클래스 로더 : 기본 자바 API들을 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드한다.
  • 시스템 클래스 로더 : 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.
  • 사용자 정의 클래스 로더 : 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.

클래스 로드 단계

클래스 로드 단계

  • 로드 : 클래스를 파일에서 가져와서 JVM 메모리에 로드한다.
  • 검증 : 읽어들인 클래스가 자바 언어 명세 및 JVM명세에 명시된 대로 잘 구성되어 있는지 검사한다. 가장 복잡하고 시간이 많이 걸린다.
  • 준비 : 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
  • 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
  • 초기화 : 클래스 변수들을 적절한 값으로 초기화한다. static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.

런타임 데이터 영역

런타임 데이터 영역

런타임 데이터 영역은 JVM이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다. PC Register, JVM stack, Native Method Stack은 스레드마다 하나씩 생성되며 Heap, Method Area, Runtime Constant Pool은 모든 스레드가 공유해서 사용한다.

  • PC 레지스터 : PC 레지스터는 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC레지스터는 현재 수행중인 JVM명령의 주소를 갖는다.
  • JVM 스택 : JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다. printStackTrace() 등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.

JVM 스택 구성

 

  • 스택 프레임(StackFame) : JVM 내에서 메서드가 수행될때 마다 하나의 프레임이 생성되어 해당 스레드의 JVM스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다. 각 스택프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.
  • 지역 변수 배열(Local Variable Array) : 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스의 인스턴스 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다.
  • 피연산자 스택(Operand Stack) : 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일 할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일시에 결정된다.
  • 네이티브 메서드 스택 : 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C스택이나 C++스택이 생성된다.
  • 힙 : 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다.
  • 메서드 영역 : 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생선된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, static 변수, 메서드의 바이트코드 등을 보관한다.
  • 런타임 상수 풀 : JVM동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM명세에서도 따로 중요하게 기술한다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메로리상 주소를 찾아서 참조한다.

 

참조