Study/Java

[Java] JVM의 개념과 구조

jonghne 2024. 1. 16. 23:04

JVM이란 ? 

JVM이란 Java Virtual Machine의 약자로 자바 바이트 코드를 읽어서 기계어로 변환하고 실행시키는 역할을 한다.
 
Java는 윈도우, 리눅스 등 어떠한 OS에서도 독립적으로 실행할 수 있는데, 이것이 가능한 이유는 JVM이 OS와 애플리케이션 중간에서 중재자 역할을 하며 OS에 맞는 기계어로 번역해주기 때문이다. (단, JVM은 OS 별로 존재한다)
 
JVM은 javac 컴파일러로 컴파일된 바이트 코드(.class)를 읽어서 OS에게 할당받은 메모리 영역에 넣어두고, 동적으로 바이트 코드를 읽어서 네이티브 언어로 해석한 뒤 실행시킨다.
 
이외에도 Garbage Collector를 통한 메모리 관리, 네이티브 메서드 지원, 멀티 스레드를 지원하는 등 많은 역할을 한다. 

JVM 구조

JVM은 크게 클래스 로더, 실행 엔진, 메모리(Runtime Data Area), 네이티브 메서드 인터페이스(JNI)와 네이티브 메서드 라이브러리로 이루어져 있다.

 

클래스 로더 (Class Loader)

클래스 로더는 클래스 파일을 읽어서 메모리에 저장하는 JVM 서브 시스템이다.
 
자바는 한번에 모든 클래스를 메모리에 올려놓지 않고 필요할 때 마다 메모리에 올리는 동적으로 로딩하는 특징이 있다.
 
즉, 특정 클래스의 인스턴스가 생성되거나, 클래스 내의 static 변수 또는 메서드가 호출되는 경우 메모리에 올라와 있는지 확인하고, 만약 없다면 메모리에 올리는 과정을 진행하는데 이 때 사용되는 것이 클래스로더 이다.
 
클래스 로더는 로딩(Loading), 링크(Linking), 초기화(Initialization) 3가지 주요 기능을 통해 동작한다.

 

1. 로딩(Loading)

클래스 로더는 계층형 구조로 Bootstrap ClassLoader / Platform(Extention) ClassLoader /Application ClassLoader 3가지 클래스 로더가 존재한다. 이 클래스 로더들은 서로 클래스를 탐색하는 경로가 다르고 우선순위에 따라 실행된다.

클래스 로딩 과정은  Bootstrap class loader부터 Application ClassLoader 순으로 순차적으로 스캔해서 클래스 파일을 찾고, 찾은 클래스 파일의 바이트 코드를 읽어서 FQCN / 클래스 타입 / 메서드, 변수와 같은 클래스 레벨의 정보를 메소드 영역에 저장한다.

  • 주로 우리가 작성하는 코드들은 Application ClassLoader에 의해 로딩된다.
  • 만약 Application ClassLoader까지 클래스를 못찾으면 ClassNotFoundException 에러가 발생한다.

 

그리고 클래스 로딩이 끝나면 해당 클래스 타입의 Class 객체를 생성해서 힙 영역에 저장한다.

 

2. 링크(Linking)

클래스 로더에 의해 클래스 파일을 로드한 이후에는 Verify, Prepare, Resolve 3가지 과정을 수행하는 링크(Linking) 단계가 진행된다.

  • Verify : 클래스 파일(바이트코드)이 자바 언어 명세 및 JVM 명세에 맞게 구성되어 있는지 검사한다.
  • Prepare : 클래스가 필요로 하는 메모리를 할당한다 (메서드 테이블을 위한 메모리 할당 및 static 변수 기본값으로 초기화)
  • Resolve : 클래스의 심볼릭 레퍼런스를 실제 메모리의 레퍼런스로 교체한다.

 

3. 초기화(Initialization)

클래스 파일의 코드를 읽고, 클래스 내의 Static 변수에 코드에 명시한 값을 할당하거나 정적 초기화 블록이 실행된다.
 

메모리 (Runtime Data Area)

JVM 메모리 영역은 크게 메서드, 힙, 스택, PC Register, 네이티브 메서드 스택 총 5가지 영역으로 나뉘어져 있다. 
 
메서드와 힙은 모든 스레드가 공유하는 영역이고, 나머지 영역은 각 스레드 별로 관리된다는 특징이 있다.

 

1. 메서드 영역

메서드 영역에는 인스턴스 생성을 위한 클래스 구조, 부모 클래스명, 메서드, 변수와 같은 클래스 레벨의 정보런타임 상수 풀 그리고 정적 변수를 저장하는 영역이다.
 
메서드 영역은 JVM 구동 시 생성되어 종료시 까지 유지되며 모든 스레드가 공유해서 사용하는 영역이다.
 

2. 힙 영역

힙 영역은 객체의 실제 인스턴스가 저장되는 공간으로, Stack 영역에서 참조되어 사용되는 공간이다.
 
메서드 영역과 마찬가지로 JVM 구동 시 생성되고 종료 시 소멸되는 스레드 간 공유하는 영역이다. 

https://code-factory.tistory.com/48

힙 영역은 크게 Young Generation(Eden, Survivor), Old Generation으로 나뉜다.

 

Young Generation은 생성된 지 얼마 되지 않은 객체가 저장되는 영역으로 minorGC가 발생하는 영역으로

해당 영역에서는 메모리가 다 차면 minorGC가 발생해서 참조되지 않는 객체를 제거하고, 사용하고 있는 객체들은 Survivor 영역에서 옮겨가며 살아남은 스코어 값이 증가된다.

 

Old Generation 영역은 Young Generation에서 스코어값이 특정 수치를 만족하는 객체들이 옮겨지는 영역으로

생성한지 오래된 객체들이 머무르는 영역이고, 해당 영역의 메모리가 다 차면 majorGC가 발생한다.

 

추가로 JDK 8 이전에 클래스의 메타데이터 정보를 담는 Perm 영역이 있었는데, 현재는 Native 메모리 영역의 Metaspace라는 영역으로 분리되었다.

 

변경된 이유는 Perm 영역이 고정된 크기를 할당 받아 GC가 자주 발생하고 OOM 오류가 발생한다는 문제가 있었는데 이를  Native 메모리 영역으로 분리해서 동적으로 메모리를 할당받기 위함이다.

  

3. 스택 영역

메서드가 호출될 때 마다 수행 정보(메서드 호출 주소, 매개변수, 지역변수, 연산 스택)가 Frame이라는 단위로 저장되는 영역이다. 
 
각 스레드 별 1개씩 스택 영역이 존재하고, 각 스택 영역 내의 Frame은 메서드 호출 시 생성 된 뒤 종료되면 제거된다.
 

4. PC Register

스택에서 현재 JVM이 실행하고 있는 바이트 코드의 메모리 주소값을 저장하는 공간으로, 스레드 마다 자신의 PC Register를 가진다. (여기서 말하는 주소값은 코드 캐시 영역 내의 주소값이다)

 
스레드가 생성될 때(start) PC Register도 생성되어 create 상태가 되고, 스레드에서 메서드 호출이 발생해서 스택 프레임에 쌓인 다면 호출된 메서드의 주소값을 저장한다. 
 
만약 자바 메서드가 아닌 Native 메서드라면 현재 실행되는 바이트코드의 메모리 주소 값을 저장하지 않고 undefined로 기록한다. 
 

5. Native 메서드 스택

자바 언어로 작성된 메서드가 아닌 C, C++과 같은 네이티브 언어로 작성된 메서드를 실행할 때 저장되는 영역으로 스레드마다 별도로 생성 및 관리된다.
 

실행 엔진 (Execution Engine)

자바의 모든 코드는 JVM에 메모리에 로드된 후 실행 엔진에 의해 실행된다. 
이때, 실행 엔진은 바이트 코드를 명령어(Instruction) 단위로 읽어서 각각 OS에 맞는 기계어로 해석하고 실행한다.

 
실행 엔진은 인터프리터와 JIT 방식을 사용해서 바이트 코드를 읽고 실행시킨다.
 
인터프리터는 초기에 나온 방식으로 바이트 코드를 명령어 하나씩 읽어서 순차적으로 실행시키는 방법이다.
이 방법은 동일한 메서드를 여러번 수행하더라고 매번 해석하고, 기계어로 변환하는 과정을 거치기 때문에 상황에 따라 속도가 느릴 수 있다는 단점이 있다. 
 
이러한 단점을 보완하기 위해 도입된 것이 JIT(Just-In-Time) 컴파일러다.
 

JIT 컴파일러는 인터프리터 방식으로 바이트 코드를 해석하고 실행하다가 반복되는 코드 블록이 있는 경우 적절한 시점에 네이티브 코드로 컴파일 해서 메모리 코드 캐시에 저장해두고, 이 후에 동일한 바이트 코드를 실행할 때에는 해당 캐시에서 네이티브 코드를 바로 실행 시키는 기법이다. 

 

이 JIT 컴파일러는 메서드가 반복되어 수행되는 경우 캐시 덕분에 성능이 좋지만, 코드 블럭을 미리 해석하는 과정에서 기존 인터프리터 보다 훨씬 많은 시간이 소요된다는 단점이 있다. 

 

네이티브 메서드 인터페이스 & 네이티브 메서드 라이브러리

네이티브 메서드 라이브러리란 C,C++,어셈블리와 같은 언어로 작성된 메서드를 재사용할 수 있게 모아서 제공하는 것을 말하고, 이 라이브러리를 자바 애플리케이션 내에서 사용할 수 있게 인터페이스를 제공하는 것이 네이티브 메서드 인터페이스 (JNI)이다

https://minhyeokism.tistory.com/27

 

자바 애플리케이션 내에서 네이티브 메서드는 항상 JNI를 통해 사용할 수 있고, JNI를 통해 호출된 네이티브 메서드는 Runtime Data Area 네이티브 메서드 스택 영역에 쌓인다 

 
 
 
참고