숨숨 베이스

지식이 숨어있는 공간

JVM

Last updated on November 30, 2025

1. JVM (Java Virtual Machine) 이란?

자바 바이트 코드를 사용 중인 OS가 알아들을 수 있는 기계어로 변환하여 해당 OS에서 구동시키는 가상 머신
이다.
image.png

(1) JVM의 특징

JVM은 Java 언어에게 플랫폼 독립적인 특성을 부여한다.
Java 코드를 실행 중인 OS 환경에 맞게 현지화 하는 책임은 JVM이 진다. 따라서 Java 코드 작성 시에는 그것이 돌아갈 환경을 염두할 필요가 없으므로 Java는 플랫폼 독립적인 특성을 가질 수 있다. 해당 특성은 비단 Java 뿐만 아니라 JVM을 활용하는 언어 (스칼라, 코루틴 등)에서 모두 보이는 특성이다.
❓ 플랫폼 의존적인 언어는 어떻게 다른가?

플랫폼 의존적인 언어의 대표격으로 C, C++을 들 수 있다. 해당 언어들은 다음과 같은 애로사항이 있다.
  1. 개발 단게에서 OS 환경 별 맞춤 컴파일러로 다 따로 컴파일 해줘야 한다. (개발 시 번거로움)
  1. 맞춤 OS 현지화를 해주는 단계가 없기 때문에, 소스코드 작성 단계에서 실행될 OS의 API, 경로 작성법, 인코딩을 염두해야 한다. 이는 OS 별로 코드를 따로 작성해야 하는 번거로움을 준다.
    1. image.png
 

2. JVM의 거시적인 작동 방식

image.png
  1. 자바 프로그램 실행 시 JVM이 OS로부터 메모리를 할당 받음
  1. 자바 컴파일러가 소스코드를 바이트 코드로 컴파일
  1. 클래스 로더가 동적으로 현재 프로그램 실행에서 필요한 클래스를 RDA(Runtime Data Area)에 적재
  1. RDA에 적재된 클래스들은 실행 엔진에 의해 실시간으로 기계어로 변환되고 OS에서 구동됨.
  1. 실행 엔진이 확인했을 떄, 필요하지만 메모리에 적재되지 않은 클래스를 RDA에 적재해줄 것을 클래스로더에게 요청

3. JVM의 미시적인 동작방식

image.png

(1) 클래스 로더

클래스로더는 다음 과정을 거쳐서 바이트 코드의 클래스를 JVM 메모리에 올리고 초기화까지 진행한다.
image.png

A. 로딩

클래스 로더가 필요한 .class 파일을 찾아 읽고 JVM의 MetaSpace에 클래스 정보를 저장한다.
image.png

B. 링크

JVM에 로드된 클래스 파일을 실제로 사용 가능하고, 안전한 상태로 만드는 단계이다. 밑의 3가지 세부 단계를 가진다.

a. 검증

클래스 파일이 정확성, 안정성, 일관성을 가지는지 (오류가 없는지) 확인하여 JVM이 손상되지 않도록 보호하는 단계이다.
  1. JVM의 Spec에 따라 클래스 파일 구조 분석
  1. 상호 참조 관계가 논리적으로 올바른지 점검
  1. JVM에서 실행할 수 없는 바이트 코드가 있는지 검사

b. 준비

클래스나 인터페이스에서 사용하는 정적 변수를 위한 메모리를 할당하고, 명시적 초기화가 없는 변수는 기본값을 설정하는 단계이다.
(즉 static int i = 10 이라고 명시적 초기화가 되어있으면 초기화 단계에서 값 설정, 이러한 초기화가 없으면 각 타입의 기본값(0,null)등을 설정)

c. 해결

클래스 파일 내부의 심볼릭 래퍼런스를 실제 다이렉트 래퍼런스로 변환하는 단계
  1. 현재 링크하려는 클래스와 관련된 심볼릭 참조를 Metaspace에서 확인.
  1. 해당 심볼릭 참조의 이름 및 시그니처를 바탕으로 클래스, 매서드, 필드, 인터페이스의 실제 메모리 주소를 찾는다.
  1. 런타임 상수풀의 구성요소들이 가진 심볼릭 참조를 (2)번에서 찾은 실제 메모리 주소로 바꾼다. (다이렉트 참조 변환), 이제 런타임 실행 중에는 빠르게 접근하도록 만든다.

C. 초기화

정적 블록이나 static 변수에 명시적 초기화를 한 값들에게 해당 값을 할당한다.

(2) 실행 엔진

런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 기계어로 변환 후 현지 OS에서 전달하여 구동시킨다. 해당 역할을 수행하는 엔진에는 인터프리터JIT 컴파일러가 존재한다. 명령어 실행 과정 중 하나의 엔진만 사용하는 것이 아니라 두가지 엔진 모두 혼합하여 사용한다.
image.png

A. 인터프리터

바이트 코드의 명령어를 하나씩 읽고 실시간 기계어 변환하여 OS에게 전달한다. (실시간 통역)
  • JVM은 기본적으로 이 인터프리터를 사용한다.
  • 인터프리터의 단점을 그대로 가지고 있음.
    -> 매번 실시간 통역해서 OS에게 전달해야 하다보니, 이미 기계어 변환이 완료된 컴파일 파일을 OS에게 전달하는 것보다 전체 수행 속도가 느림.

B. JIT 컴파일러

JIT 컴파일러는 자주 사용되는 매서드나 루프 코드를 발견하고 해당 바이트 코드 전체를 컴파일하여 Native Code로 변경하여 JVM 메모리의 JIT 코드 캐시에 저장한다. 이후 해당 매서드나 코드가 다시 호출된다면 인터프리트 과정 없이 바로 코드 캐시의 컴파일된 네이티브 코드가 직접 실행된다.
모든 바이트 코드를 Native Code로 변환한다면 분명 실행이 빠르겠지만, 해당 코드를 저장할 Code Cache 용량이 한정되어 있고, 해당 변환 과정의 비용도 기본 인터프리팅 방식보단 크다보니 일정 임계치를 넘긴 코드만 인터프리팅 방식에서 JIT 컴파일링 방식으로 넘어온다.
❓ 등장 이유?

반복적으로 사용되는 코드나 루프 코드가 매번 interpreted 과정을 거친다면, 사용 시마다 매번 interpreter의 실시간 통역 과정을 거쳐야하기 때문에 느릴 것이다. 해당 속도 저하를 보완하고자 JIT 컴파일러가 등장했다.
❓ '자주 사용되는 코드' 의 기준은?

사용 중인 JVM 스펙에 따라 다르다. 보통 매서드 호출 횟수, 루프 반복 횟수를 인수로 계산한다.

C. 가비지 컬렉터 (GC)

Heap 메모리 영역에서 더 이상 사용되지 않는 참조 타입들을 회수하여 메모리의 효율적 사용을 독려한다.
 

(3) 런타임 데이터 영역

(3-1) 런타임 데이터 영역의 역사 (java 7, java8)

image.png
Java 7까지는 JVM의 Heap 메모리 영역 중에서, 영구적으로 살아있는 메모리 영역인 pergen이 존재했고, 그 영역에 Method Area가 존재했다. 클래스 로더가 바이트 코드를 적재하는 공간이 바로 이 Method 영역이었다.
그러나 Pergen 영역의 크기는 고정적이기 때문에, 한 번에 많은 클래스가 메모리에 적재될 때 이를 다 받아들이지 못하고 OOM (Out of Memory) 오류가 자주 발생했다. 해당 문제를 해결하기 위해, Java 8부터는 메모리 구조가 다음과 같이 바뀌었다.
image.png
Method 영역이 사라지고, OS 네이티브 영역의 MetaSpace가 해당 역할을 대신하게 되었다. 즉 클래스 로더가 Metaspace에 바이트 코드를 적재하고, 링크 작업을 한다. 실행 엔진이 실시간으로 실행한 명령어의 내용은 JVM의 런타임 데이터 영역에서 이루어진다.
MetaSpace 영역은 네이티브 OS의 영역이므로, 메모리 공간이 가득찰 때마다 OS가 자동 확장을 한다. 따라서 Java 8 이전 버전까지 빈번히 발생하던 OOM 문제를 미리 방지할 수 있게 되었다.
하지만, 메타스페이스 영역이 OS의 메모리 영역마저 초과하면 시스템 전체 장애로 번질 가능성이 있다. 따라서 MetaSpace 영역의 자동 확장 최대치를 설정해주는 것이 좋다. 명령어: XX:MaxMetaspaceSize
여기서 하나 달라진 점은 문자열 상수풀은 런타임 상수풀에서 떨어져 나와 Heap에 아직 그대로 있다는 것이다.

(3-2) 스레드의 영역

image.png
바이트 코드 형태의 클래스를 가져오는 Metaspace 영역과 Heap 영역은 모든 스레드가 공유한다. 반면 PC 레지스터와 JVM 스택, Native Method Stack은 스레드 별로 독립 할당된다.위의 그림을 Thread의 관점에서 나눈다면 다음과 같이 될 것이다.
image.png

A. Metaspace 영역

역할
클래스 로더가 바이트코드를 적재하는공간

저장 정보
1.클래스의 메타 정보 (필드, 매서드 정보, 클래스 구조 정보)
2. 클래스 내 필드들의 참조 정보
내용물 생존 기간
동적으로 클래스 로더에 의해서 적재된 순간부터 언로딩 되는 순간까지 (대게 프로그램 종료시 까지)

B. Heap 영역

역할
프로그램 실행 중 동적으로 데이터를 저장하기 위해 할당되는 메모리 공간
저장 정보
참조 타입 데이터 (클래스 객체, 자료구조 등)
내용물의 생존기간
프로그램 실행 중 필요에 의해 동적으로 생성된 순간부터 누구도 참조하지 않은 데이터가 될 때까지 생존. (누구도 참조하지 않으면 GC가 수거해감)
특징
1. 스택의 변수명이 Heap 영역의 참조 객체를 핸들링한다.
2. 살아남는 기간에 따라 저장되는 영역이 다르다.

a. 스택의 변수명이 Heap 영역의 참조객체를 핸들링한다.

stackandHeap.png
Stack의 참조 변수는 주소값을 가지고 있고, 해당 주소값이 가리키는 위치에 실제 데이터를 가지고 있다. 만약 Heap에 있는 특정 객체를 어떠한 변수도 참조하지 않을 경우, 해당 객체는 쓸모 없는 데이터가 되어 GC의 대상이 된다. (즉 수거되고 할당 받았던 Heap 영역을 반납하게 된다.

b. 살아남은 기간 순으로 저장되는 영역이 다르다.

image.png
처음 참조 객체가 할당 받았을 때는 eden 영역에 존재하고, 그 뒤로 살아남을 때마다 surviovr -> Old 영역으로 옮겨진다.

C. 스택 영역

역할
매서드가 호출되어 실행될 때, 해당 매서드의 실행 과정을 표현하기 위한 임시 저장소 역할
저장 정보
1. 매서드에서 사용되는 지역변수, 인수
2. 중간 연산 결과, 피연산자
3. 해당 매서드 끝나고 돌아갈 복귀 주소, 예외처리 정보
내용물의 생존 기간
매서드 호출과 반환이 끝나면 해당 매서드를 나태는 스택 프레임이 스택에서 제거된다.
특징
1. 스레드가 새로 생길 때마다 독립적인 스택이 JVM에 새로 생성된다.
2. 매서드는 스택 프레임단위로 스택에 저장되고 POP 된다.

D. PC 레지스터

역할
각 스레드가 현재 실행 중인 바이트코드 명령어의 주소를 저장하여, 컨텍스트 스위칭 후 돌아왔을 때, 원래 하던 위치부터 재개할 수 있게 하는 역할이다.
저장 정보
바이트 코드 해석기가 명령어를 하나 실행한 뒤, PC 레지스터 값을 해당 바이트 코드의 위치로 갱신한다.
특징
1. 만약 자바가 네이티브 매서드를 실행할 떈, PC 레지스터 값은 정의되지 않을 수 있다.

E. 네이티브 매서드 스택

자바 이외의 C,C++, 어셈블리 언어로 작성되고 컴파일된 네이티브 코드를 실행하기 위한 공간
OS가 관리하는 C 스택위에 만들어지며, 여기에 네이티브 매서드용 스택 프레임이 쌓이면서 운용된다.

(3) JNI (Java Native Interface)

JavaStack에서 네이티브 코드가 필요한 경우, 네이티브 매서드 스택으로 제어 흐름을 넘기고, 네이티브 매서드 스택에서 일을 마치면 다시 JVM의 Stack으로 호출 제어를 넘길 수 있도록 돕는 인터페이스이다.
  • 자바 객체와 네이티브 데이터의 상호 변환 및 스택 간의 흐름 제어 권환 제어 역할을 한다.
    • image.png

부록

A. 모르는 것 정리

  • 런타임 상수풀
    : 클래스를 생성할 때 참조해야할 정보들을 상수로 가지고 있는 영역
    • 클래스, 인터페이스에서 사용되는 모든 상수, 매서드, 필드의 래퍼런스 정보, 그들의 심볼릭 참조
    • 클래스와 인터페이스 이름, 부모 클래스 이름 등 구조적인 정보
  • 심볼릭 참조
    : 매서드, 필드, 클래스 이름을 활용해 참조 대상이 무엇인지 표기 하는 것
    만약 String 객체를 참조하고 있다면 해당 객체의 심볼릭 참조는 java/lang/String 형태이다. 특정 내가 만든 클래스를 참조하려 경우, 심볼릭 참조는 my/package/MyClass 형태로 되어 있음.
0: new #2 // 클래스 Person의 심볼릭 참조 3: invokespecial #3 // 생성자 Person.<init>()의 심볼릭 참조 6: invokevirtual #4 // 메서드 Person.sayHello()의 심볼릭 참조 // 상수풀(Constant Pool)의 각 항목 예시: #2 = Class Person #3 = Method Person.<init>:()V #4 = Method Person.sayHello:()V
  • Native Code
    : Java 코드를 C, C++ 혹은 어셈블리 언어로 바꾸고 컴파일한 코드
  • 스택 프레임
    : 스택에 저장되는 단위로서 스택 프레임 하나가 매서드 하나를 나타낸다.
    매서드에서 사용되는 지역 변수, 파라미터, 연산 중간 결과, 피연산자 , 해당 매서드를 끝마치면 복귀하는 주소, 예외 처리 정보 등이 저장됨.

B. 참고 문서


⬅️ 이전 글
➡️ 다음 글