-
[Java] JVM의 Runtime Data AreaJava 2025. 11. 19. 17:38

이전글: JVM이 무엇이고 Java 코드는 어떻게 동작할까?
1. Runtime Data Area란?

Runtime Data Area Runtime Data Area는 JVM이 운영체제(OS) 위에서 실행될 때, OS로부터 할당받은 메모리 공간을 의미합니다.
JVM이라는 프로세스가 자바 프로그램을 실행하기 위해 필요한 모든 데이터(클래스 메타데이터, 객체 인스턴스, 지역변수, 등)를 적재하고 관리하는 논리적인 메모리 영역을 의미합니다.
이 영역은 데이터의 라이프사이클과 가시성에 따라 크게 '모든 스레드 공유 영역(per-process)'과 '스레드별 격리 영역(Per-Thread)'으로 구분됩니다.
구분 영역 이름 공유 여부 (Scope) 핵심 저장 데이터 (Content) 공유 영역
(프로세스 단위)Method Area
(메소드 영역)모든 스레드 공유
(JVM 시작 ~ 종료)클래스 메타데이터 (Type Info), Runtime Constant Pool,
Static 변수, 바이트 코드Heap Area
(힙 영역)모든 스레드 공유
(JVM 시작 ~ 종료)객체 인스턴스 (new), 배열 (Array) 개별 영역
(스레드 단위)JVM Stack
(JVM 스택)스레드별 독립
(스레드 시작 ~ 종료)스택 프레임 (Stack Frame) PC Register
(PC 레지스터)스레드별 독립
(스레드 시작 ~ 종료)현재 실행 중인 JVM 명령어의 주소 Native Method Stack 스레드별 독립
(스레드 시작 ~ 종료)네이티브 함수 실행 정보 2. 메소드 영역(Method Area)
메소드 영역은 모든 스레드가 공유하는 논리적인 메모리 영역으로, 클래스 로더(Class Loader)가 로딩한 '클래스 수준의 데이터(Class-Level Data)를 저장하는 저장소입니다.
2-1. 핵심 특징
- 공유 자원: JVM 내에서 단 하나만 존재하며, 모든 스레드가 공유합니다. 따라서 멀티 스레드 환경에서 Thread-Safe 하게 관리되어야 하는 영역입니다.
- 생명 주기(Lifecycle): JVM 구동 시 생성되며, JVM 종료될 때까지 유지됩니다.
- 논리적 위치: JVM 명세상 힙(Heap)의 논리적 일부로 간주되지만, 힙과 구분하기 위해 Non-Heap이라고도 부릅니다.
- 구현체: Oracle Hotspot JVM 기준으로 Java 8 이후에는 Metaspace로 구현됩니다.
2-2. 저장 내용
클래스 로더가 바이트 코드(.class 파일)를 읽어 메모리에 적재할 때, 파일 내부의 정보를 분해하여 다음과 같이 체계적으로 저장합니다.
① 타입 정보 (Type Information)
클래스 자체를 정의하는 메타데이터입니다.
- FQCN (Fully Qualified Class Name): 패키지명을 포함한 클래스의 전체 이름 (예: com.myproject.User)
- Super Class Name: 직계 부모 클래스의 이름 (단, Object는 제외)
- Interface Names: 구현(implements)하거나 상속(extends)한 인터페이스들의 전체 이름 목록 (List 형태)
- Modifiers: 접근 제어자 및 속성 (public, abstract, final 등)
- 객체 유형 식별 정보: 이 파일이 Class, Interface, Enum, Record, Annotation 중 무엇인지 식별하는 플래그(Flag) 정보가 포함됩니다.
② 런타임 상수 풀 (Runtime Constant Pool)
메소드 영역에서 가장 중요한 핵심 구성 요소입니다. 각 클래스와 인터페이스마다 별도로 존재합니다.
❓ 왜 '상수 풀(Constant Pool)'인가요?
여기서 말하는 '상수'는 단순히 final 변수만을 의미하는 것이 아닙니다. 프로그램이 실행되는 동안 변하지 않는 모든 '참조 정보'와 '값'을 모아놓은 풀(Pool)이라는 뜻입니다.- 기원: 바이트 코드(.class 파일) 내부의 constant_pool 테이블이 런타임 환경에서 메모리에 로드된 형태입니다.
- 내용: 크게 두 가지 종류의 데이터가 인덱스(번호)와 함께 저장됩니다.
- 리터럴 (Literal): 코드에 하드코딩된 정수, 실수, 문자열(String Pool에 있는 객체를 가리키는 Reference)
- 심볼릭 레퍼런스 (Symbolic Reference): 클래스, 필드, 메소드의 이름과 타입 정보.
- 동작 원리 (동적 링킹):
- JVM은 바이트 코드에 있는 이 상수 풀의 인덱스 번호(예: #2)를 통해 대상을 지칭합니다.
- 실행 엔진이 이 인덱스를 참조하면, JVM은 상수 풀에 있는 '이름(Symbol)'을 확인합니다.
- 이 이름을 통해 실제 메모리에 로드된 클래스의 '물리적 주소(Direct Reference)'를 찾아 연결해 줍니다.
- 이 과정을 동적 링킹(Dynamic Linking)이라고 합니다.
③ 필드 정보 (Field Information)
클래스 멤버 변수(필드)에 대한 명세입니다. 다음을 저장합니다.
- 필드 이름, 데이터 타입, 필드 접근 제어자
- 순서: 필드가 선언된 순서대로 기록
- 메모리 절약을 위해 필드의 이름과 타입 문자열을 직접 저장하지 않고, 런타임 상수 풀의 인덱스를 저장하여 참조합니다.
④ 메소드 정보 (Method Information)
메소드의 선언부와 구현부가 저장됩니다.
- 메소드의 이름, 리턴 타입, 파라미터 수와 타입
- 필드 정보와 마찬가지로 이름과 타입 정보는 런타임 상수 풀의 인덱스를 가리킵니다.
- 접근 제어자
- 메소드의 바이트코드
- 실제 컴파일된 명령어 코드입니다.
- 실행 엔진(Execution Engine)은 PC Register가 가리키는 주소를 통해 이곳에 저장된 바이트코드를 가져와 실행합니다.
- 피연산자 스택의 최대 깊이, 지역 변수 최대 개수, 예외 테이블 등이 저장됩니다.
⑤ 클래스 변수 (Class Variables / Static Variables)
static 키워드로 선언된 변수입니다.
이 변수들은 인스턴스(Heap)에 속하지 않고 클래스 자체에 속하며, 클래스가 로딩될 때 값이 할당되고 프로그램 종료 시까지 유지됩니다.(final static은 런타임 상수 풀에 상수(Constant)로 저장되어 관리됩니다.)
3. 힙 영역 (Heap Area) 상세 분석
3-1. 힙 영역이란?
힙 영역은 모든 스레드가 공유하는 객체 인스턴스와 배열의 저장소입니다.
- 역할: new 연산자로 생성된 모든 객체(Object)와 배열(Array)이 이곳에 저장됩니다.
- 특징:
- JVM 메모리 중 가장 큰 공간을 차지합니다.
- GC(Garbage Collector)의 주요 관리 대상입니다.
- 모든 스레드가 공유하므로 동기화(Synchronization) 문제가 발생할 수 있습니다.
3-2. 힙의 내부 구조
효율적인 메모리 관리와 GC 최적화를 위해 힙은 객체의 생존 기간에 따라 Young Generation과 Old Generation으로 물리적으로 나뉩니다.
❓ 왜 나누었나요?
GC 설계의 기반이 되는 Weak Generational Hypothesis를 통해 알 수 있습니다.
- 대부분의 객체는 생성되자마자 접근 불가능 상태(Unreachable)가 된다.(금방 쓰레기가 됨)
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다. 이 가설 덕분에 전체 힙을 매번 뒤지는 대신, Young 영역만 자주 스캔하는 것만으로도 메노리를 효율적으로 확보할 수 있다.
Reachable: GC Root에서 시작하여 참조 사슬(Reference Chain)을 따라갔을 때, 도달할 수 있는 객체
Unreachable: GC Root에서 시작하는 참조 사슬이 끊겨서, 어떤 경로로도 접근할 수 없는 객체
GC Root: 판단의 시작점이 되는 객체들입니다. 주로 Stack 영역의 지역 변수, Method Area 영역의 Static 변수, Native Stack의 JNI 참조 등이 이에 해당합니다.① Young Generation
- 설명: 객체가 최초로 할당되는 공간입니다. 전체 힙의 일부를 차지하며, GC가 매우 빈번하게 일어납니다.
- 특징
- 이곳에서 일어나는 GC를 Minor GC라고 합니다.
- Copying 알고리즘을 사용하여, 살아남은 객체를 다른 곳으로 복사하고 나머지를 한 번에 비웁니다. 이 덕분에 Young 영역은 메모리 단편화(Fragmentation / 빈 공간은 많은데, 정작 쓸 수 있는 큰 공간이 없는 상태)가 발생하지 않습니다.
- 세부 구조:
- Eden: new를 통해 객체가 생성되는 최초의 공간입니다.
- Survivor 0 / 1 (S0, S1): Eden에서 살아남은 객체들이 이동하는 곳입니다. 메모리 단편화를 막기 위해 두 영역 중 하나는 반드시 비어 있는 상태를 유지해야 합니다. Minor GC가 발생할 때마다 살아남은 객체들을 비어 있는 영역으로 복사하고, 기존 영역을 비우는 역할 교대가 반복됩니다.
② Old Generation
- 설명: Young 영역에서 오랫동안 살아남아 승격된 객체들이 저장되는 공간입니다.
- 특징:
- Young 영역보다 크기가 크게 설정됩니다.
- 이곳이 꽉 차면 Major GC(또는 Full GC)가 발생합니다.
- 이때 애플리케이션이 일시적으로 멈추게 되므로, 성능 튜닝의 핵심 대상입니다.
3-3. 객체의 일생 (Life Cycle in Heap)
객체가 메모리 할당(Allocation)되고, 이동(Copying)하고, 승격(Promtion)되는 과정을 정리해 보겠습니다.
- 탄생 (Allocation):
- new 연산자가 실행되면 객체는 Eden 영역에 할당됩니다.
- 이때 TLAB를 사용하여 스레드 간 동기화 없이 매우 빠르게 할당됩니다.
- 생존 경쟁 (Minor GC):
- Eden이 꽉 차면 Minor GC가 트리거됩니다.
- JVM은 살아있는 객체(Reachable)를 식별(Mark)합니다.
- 살아남은 객체는 비어있는 Survivor 영역으로 복사(Copy)되어 이동합니다.
- Eden 영역과 기존 Survivor 영역의 나머지 쓰리기 객체들이 전부 비워집니다.
- 성장 (Aging):
- Survivor 영역으로 이동한 객체는 객체 헤더(Object Header)에 있는 Age bit(나이)를 1 증가시킵니다.
- 다음 Minor GC때도 살아남으면 S0<->S1을 왔다 갔다 하며 나이를 계속 먹습니다.
- 승격 (Promotion):
- 객체의 나이(Age)가 임계값을 넘어서면, Old Generation으로 이동합니다. 이를 promotion이라고 합니다.
- 최후 (Major GC):
- Old 영역의 메모리가 부족해지면 Major GC가 발생합니다.
- 주로 Mark-Sweep-Compact 알고리즘을 사용하여 힙 전체를 정리하며, 이때 진행하는 동안 서버가 멈출 수 있습니다.
- 여기서도 참조되지 않으면(Unreachable) 메모리에서 해제됩니다.
4. JVM 스택
JVM 스택은 각 스레드가 메소드를 실행할 때 사용하는 독립적인 공간입니다.
각 스레드마다 하나씩 존재하며, 스레드가 시작될 때 생성되는 스레드 격리(Thread-Private) 영역입니다.
4-1. 핵심 특징
1. 스레드 한정(Thread Confinement)
- 각 스레드가 자신만의 스택을 가지므로, 다른 스레드가 접근할 수 없습니다. 따라서 동기화(Synchronization) 처리가 필요 없으며, 완벽하게 Thread-Safe 합니다.
2. LIFO 구조(Last-In-First-Out)
- 자료구조 '스택'의 특성 그대로, 가장 나중에 호출된 메소드가 가장 먼저 처리되고 나가는 구조입니다.
3. 프레임 단위 관리
- 스택에 저장되는 데이터의 기본 단위는 스택 프레임(Stack Frame)입니다. 메소드 하나당 하나의 프레임이 할당됩니다.
4-2. 스택 프레임
스텍 프레임이란?
스텍 프레임(Stack Frame)은 JVM 스택 내에 만들어지는 하나의 블록으로, “하나의 메소드가 실행되는 동안 데이터와 상태 정보를 저장하는 영역”입니다. 메소드가 호출(Invoke)될 때 생성(Push)되고, 실행이 완료되거나 예외가 발생하면 종료되면 소멸(Pop)됩니다. 각 프레임은 자신만의 지역 변수 배열, 피연산자 스택, 프레임 데이터를 가집니다.
지역 변수 배열(Local Variable Array)메소드 실행에 필요한 데이터를 담는 인덱스 기반의 배열입니다.
0번 인텍스부터 차례대로 데이터가 들어가며 다음과 같은 저장 순서를 가집니다.
1. this(인스턴스 메소드인 경우): 0번 인덱스에는 항상 현재 객체의 참조(this)가 저장됩니다. (static 메소드인 경우 this가 없기 때문에 0번 인덱스부터 바로 첫 번쨰 매개변수가 저장됩니다.
2. 매개 변수(Parameters): 메소드 호출 시 넘겨받은 인자값들(args)이 순서대로 저장됩니다.
3. 지역 변수(Local Variable): 메소드 내부에서 선언된 변수들이 순서대로 저장됩니다.
boolean, char, int 등의 기본 자료형은 값을 직접 저장하고, 이외의 참조 자료형들은 힙(Heap)에 저장한 후 참조 주소값을 저장합니다.
피연산자 스택(Operand Stack)
메소드가 실제로 계산(연산)을 수행하는 공간입니다. JVM은 레지스터 기반이 아닌 스택 기반으로 작동합니다. 즉, 계산을 위해 데이터를 쌓았다가 꺼내는 방식으로 작동합니다.
동작 예시: int c = a + b
Load: 지역 변수 배열에서 a값을 가져와 피연산자 스택에 넣습니다.(Push)
Load: 지역 변수 배열에서 b값을 가져와 피연산자 스택에 넣습니다.(Push)
Add: 연산 명령이 실행되면, 스택에서 두 값을 꺼내(Pop) 더합니다.
Push: 결과값을 다시 스택에 넣습니다.(Push)
Store: 스택에 있는 결과값을 꺼내(Pop) 지역 변수 배열 c 위치에 저장합니다.
프레임 데이터(Frame Data) & 동적 링킹메소드 실행을 지원하고, 종료 후 복귀를 돕는 관리 정보들이 포함됩니다.
동정 링킹(Dynamic Linking)은 현재 실행 중인 메소드가 속한 클래스의 런타임 상수 풀(Runtime Constant Pool) 에 대한 참조입니다. 메소드 내에서 다른 클래스의 멤버나 메소드를 호출할 때, 이 참조를 통해 상수 풀로 가서 실제 주소를 찾아옵니다.
메소드 리턴 주소(Return Address)는 메소드가 종료된 후, 돌아가야 할 호출 지점(PC Register 값)을 저장합니다. 정상적으로 종료 시 이 주소로 복귀하여 다음 명령어를 실행합니다.5. Native Method Stack
자바가 아닌 다른 언어(C, C++, Assembly 등)로 작성된 네이티브 코드를 실행하기 위한 스택 영역
5-1. 핵심 정의 및 특징
1. C Stacks
- JVM 스택이 자바 바이트코드를 위한 공간이라면, 이 스택은 일반적인 C 프로그램이 실행될 때 사용하는 스택과 구조가 동일합니다.
2. 스레드 격리(Thread-Private)
- JVM 스택과 마찬가지로 스레드별로 생성됩니다.(Thread Safe)
- 다른 스레드가 침범할 수 없는 독립적인 영역입니다.
3. JNI 의존성
- 이 스택은 독자적으로 동작하지 않고 반드시 JNI(Java Native Interface)를 통해 호출됩니다.
4. JVM 구현체 종속
- 구체적인 구현은 각 벤더사들의 JVM 구현체마다 다릅니다.
5-2. 동작 매커니즘
자바 코드에서 native 메소드를 호출할 때, JVM 내부에서 스택 전환이 일어납니다.
1. 호출(invocation)
자바 프로그램이 실행되다가 native 키워드가 붙은 메소드를 만납니다.
2. 동적 링킹(JNI Linking)
실행 엔진은 JNI를 통해 해당 메소드와 매핑되는 네이티브 라이브러리 파일 내의 함수를 찾습니다.
3. 컨텍스트 전환
- JVM 스택 정지: JVM은 현재까지의 작업 내용(Java Stack Frame)을 그대로 둔 채, 실행의 흐름을 네이티브 메소드 스택으로 옮깁니다.
- 네이티브 프레임 생성: 네이티브 스택에 새로운 프레임이 생성되고, 여기서부터는 자바 바이트 코드가 아닌 native 명령어가 실행됩니다.
- 확장: 네이티브 함수 내에서 다시 자바 메소드를 호출할 수 있습니다. 이때는 다시 JVM 스택에 프레임이 쌓입니다.
4. 실행 및 변환
C, C++ 로직이 수행된 후, 결과값은 JNI를 통해 자바 데이터 타입으로 변환되어 JVM 스택으로 반환됩니다.
6. PC Register
현재 실행 중인 JVM 명령어의 주소값을 저장하는 스레드별 독립 공간
6-1. 핵심 역할 및 특징
1. 스레드 격리
- JVM 스택과 마찬가지로 각 스레드마다 하나씩 생성됩니다.
- 다른 스레드가 접근할 수 없으며, 스레드가 시작될 때 생성되고 종료될 때 소멸합니다.
2. 역할
- 실행 엔진에게 다음에 실행할 바이트 코드 명령어의 위치를 알려주는 이정표 역할을 합니다.
- 현재 수행 중인 JVM 명령어의 주소를 가리킵니다.
3. 존재 이유: 컨텍스트 스위칭
- 멀티 스레딩 환경 때문에 스레드마다 필요
- 상황: CPU 코어는 한 번에 하나의 스레드만 실행할 수 있습니다. 따라서 아주 짧은 시간 단위로 여러 스레드를 번갈아 가며 실행합니다(context switching)
- 문제: 스레드 A가 작업을 하다가 스레드 B로 전환되었습니다. 잠시 후 다시 스레드 A 차례가 되었을 때 아까 어디까지 실행했는지 모르는 문제가 있습니다.
- 해결: 스레드 A는 자신의 PC Register에 지금 실행중인 명령어에 대한 정보를 기록해두고 CPU를 비워줍니다. 추후에 다시 작업할 때 PC Register에 기록해둔 위치를 바탕으로 작업을 재개합니다.(바이트 코드의 논리적 주소 기준으로)
'Java' 카테고리의 다른 글
[Java] Sync vs Async (0) 2025.12.01 [Java] Blocking/Non-blocking이란? (0) 2025.12.01 [Java] 프로세스(Process)와 스레드(Thread) (1) 2025.11.27 [Java] JVM이 무엇이고 Java 코드는 어떻게 동작할까? (0) 2025.11.18 SOLID 원칙 완벽 정리 (3) 2025.06.18