JVM 내부 구조

정호윤·2024년 2월 14일
0

Java Virtual Machine

목록 보기
2/3

자바 가상 머신(Java Virtual Machine, 이하 JVM)은 자바 프로그램을 실행하기 위한 가상 머신입니다. JVM은 클래스 로더, 메모리, 실행 엔진으로 이루어져 있습니다.

자바 프로그램을 실행하면 JVM 프로세스가 시작됩니다. 이때 클래스 로더는 프로그램 실행에 필요한 클래스 파일들을 런타임 데이터 영역에 로드합니다. 그 후 실행 엔진은 런타임 데이터 영역에 로드된 바이트코드를 실행합니다.

JVM 덕분에, 클래스 파일로 컴파일된 자바 코드는 어떤 플랫폼에서도 재컴파일 없이 실행될 수 있습니다.


클래스 로더

클래스 로더는 클래스 파일을 JVM의 메모리에 동적으로 로드합니다. 일반적으로 클래스 로더는 현재 필요한 클래스 파일만 로드하며, 다음 동작을 순서대로 수행합니다.

  1. 로드: 클래스 로더가 클래스 파일을 읽고 클래스나 인터페이스를 생성해 JVM의 메서드 영역에 저장합니다.
  1. 링크: 클래스나 인터페이스가 JVM의 런 타임 상태에서 실행될 수 있도록 합니다. 링킹은 검증(verification), 준비(preparation), 결정(resolution)의 순서대로 일어납니다.
    • 검증: 클래스 파일의 형식이 유효한지 확인합니다.
    • 준비: 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 자료구조를 준비합니다.
    • 결정: 클래스 상수 풀 내의 모든 심볼릭 참조를 직접 참조로 변환합니다.
  1. 초기화: 초기화 메서드를 실행함으로써 클래스의 static 필드에 초기값을 할당합니다.

Note
심볼릭 참조란 참조하는 대상의 실제 메모리 주소가 아닌 이름이나 식별자를 참조하는 것입니다.


런타임 데이터 영역

런타임 데이터 영역은 JVM 프로세스가 운영체제 위에서 실행되며 할당받는 메모리 영역이며, 자바 프로그램의 실행에 필요한 데이터가 저장됩니다. 런타임 데이터 영역 중 일부는 JVM 프로세스와 생명주기를 같이합니다. 나머지 영역들은 쓰레드와 생명주기를 같이합니다.

PC 레지스터

PC(Program Counter) 레지스터는 각 쓰레드마다 존재하며, 현재 실행 중인 바이트코드의 주소가 저장됩니다. 만약 쓰레드에서 실행 중인 메서드가 네이티브 메서드라면, PC 레지스터의 값은 정의되지 않습니다. 네이티브 메서드란 자바 이외의 언어로 작성된 메서드입니다. 이러한 메서드는 JVM 외부에 구현되어 있으므로 PC 레지스터의 값을 정의할 수 없습니다.

JVM 스택

JVM 스택은 각 쓰레드마다 존재하며, JVM 스택에는 메서드 호출과 관련된 정보를 저장하는 프레임이라는 자료구조가 저장됩니다. JVM은 오직 JVM 스택에 프레임을 추가하고(push) 제거하는(pop) 동작만 수행합니다. 메서드가 호출되면 JVM은 프레임을 생성해 해당 쓰레드의 JVM 스택에 추가합니다. 반대로, 메서드가 종료되면 해당 프레임은 JVM 스택에서 제거됩니다. 프레임에는 지역 변수 배열, 피연산자 스택, 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 참조 등이 저장됩니다.

지역 변수 배열은 0-indexed 배열이며, 0번 인덱스에는 메서드가 속한 클래스 인스턴스에 대한 참조인 this가 저장됩니다. 이후 인덱스에는 메서드 파라미터 및 지역 변수들이 순차적으로 저장됩니다.

피연산자 스택은 메서드의 작업 공간입니다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하기도 하고, 다른 메서드의 리턴값을 추가(push) 및 제거(pop)하기도 합니다. 이처럼 JVM은 레지스터가 아닌 스택을 기반으로 동작합니다.

힙(Heap)은 클래스 인스턴스와 배열이 저장되는 메모리 영역입니다. 힙은 JVM 시작 시 생성되며, 모든 JVM 쓰레드에서 공유됩니다. 객체의 힙 스토리지는 자동 스토리지 관리 시스템인 가비지 콜렉터(Garbage Collector, GC)에 의해 관리됩니다. GC는 더 이상 사용되지 않는 객체들을 제거해 효율적인 메모리 사용을 돕습니다.

메서드 영역

메서드 영역에는 클래스 파일에서 읽어 들인 클래스나 인터페이스의 런타임 상수 풀, 메서드 및 필드 정보, 바이트코드 등이 저장됩니다. 메서드 영역은 JVM 시작 시 생성되며, 모든 JVM 쓰레드에서 공유됩니다.

런타임 상수 풀

런타임 상수 풀은 클래스 파일에 있는 상수 풀 테이블의 런타임 표현입니다. 즉, 런타임 상수 풀은 상수와 필드, 메서드에 대한 참조가 저장되어 있는 테이블입니다.

각 타입의 런타임 상수 풀은 해당 클래스 파일이 JVM에 의해 로드될 때 메서드 영역에 저장됩니다. JVM이 특정 필드나 메서드를 참조해야 할 때, JVM은 해당 메서드나 필드의 실제 메모리 주소를 런타임 상수 풀에서 찾아 참조합니다.

네이티브 메서드 스택

네이티브 메서드 스택은 자바 이외의 언어로 작성된 네이티브 코드를 위한 스택입니다. JVM은 네이티브 메서드를 지원하기 위해 C나 C++ 스택을 사용할 수 있습니다. 각 쓰레드는 별도의 네이티브 메서드 스택을 갖습니다.


실행 엔진

실행 엔진은 런타임 데이터 영역에 저장된 바이트코드를 읽고 실행합니다. 바이트코드를 변환하는 방식에는 인터프리터를 사용하는 방식과 JIT(Just-In-Time) 컴파일러를 사용하는 방식이 있으며, JVM은 이를 적절히 혼합하여 사용합니다.

실행 엔진은 또한 가비지 콜렉터(Garbage Collector, GC)라는 자동 메모리 관리 시스템을 통해 더 이상 사용되지 않는 객체의 메모리를 해제합니다.

인터프리터

인터프리터는 바이트코드를 하나씩 읽어 JVM에서 직접 실행합니다. 일반적으로 인터프리터는 바이트코드를 변환하지 않고 바로 실행하기 때문에 빠릅니다. 하지만 캐싱이 불가하므로 동일한 코드가 여러 번 실행되면 성능이 저하되는 문제가 있습니다.

JIT 컴파일러

JIT 컴파일러는 인터프리터의 단점을 보완하기 위해 도입되었습니다. JIT 컴파일러는 프로그램의 실행 중에 바이트코드 전체를 컴파일해 호스트의 기계어로 변환합니다. 물론 컴파일 시에는 약간의 딜레이가 존재합니다. 하지만 컴파일이 완료된 후에는 더 이상 바이트코드를 인터프리트 하지 않고 캐싱된 기계어를 실행하므로 인터프리터보다 훨씬 빠릅니다.


JVM 구조 정리

이제 지금까지 학습했던 개념들을 떠올리며 아래 그림을 살펴보세요.


출처

0개의 댓글