J-S-06 JVM 기본 이론 및 메모리 관리 메커니즘
글 정보
- 카테고리
- Programming/Java/Starter
- 태그
- JavaLevel2
1. JVM의 본질과 시스템 내 위치
1.1. 컴퓨터 구조와 JVM (User Space vs Kernel Space)
가장 먼저 이해해야 할 것은 "JVM은 어디에 있는가?"입니다.
PC 시스템은 크게 물리적인 하드웨어(HW)와 논리적인 소프트웨어(SW)로 나뉩니다.
- User Space (사용자 영역): JVM은 이곳에서 하나의 프로세스(Process)로 실행됩니다.
- Kernel Space (커널 영역): 운영체제(OS)가 상주하며 하드웨어(CPU, Memory)를 제어합니다.
- Native 영역: OS와 하드웨어를 합쳐 네이티브 영역이라 부르며, JVM은 원칙적으로 이 영역에 직접 명령을 내릴 수 없습니다. (단, JNI를 통하는 경우는 예외)

즉, JVM은 "OS 이론과 컴퓨터 구조론을 소프트웨어적으로 구현(짬뽕)해 놓은 가상 머신" 입니다.
실제 하드웨어가 없지만, 소프트웨어적으로 CPU, Register, Memory 등을 흉내 내어 만든 논리적 컴퓨터라고 볼 수 있습니다.
1.2. C++와 Java의 메모리 관리 철학 차이
두 언어는 메모리를 대하는 태도가 정반대입니다.
- C++ (개발자 중심):
- 객체의 생성부터 해제까지 개발자가 코드 레벨에서 모두 관여해야 합니다.
- 장점: 극한의 성능 최적화가 가능합니다.
- 단점: 실수할 경우 메모리 누수(Memory Leak) 등 치명적인 문제가 발생합니다.
- Java (JVM 위임):
- "비즈니스 로직에만 집중하세요."
- 실제 메모리 해제는 전적으로 JVM(Garbage Collector)의 몫입니다.
- 개발자에게 메모리 제어 권한(해제 권한)을 주지 않았습니다.
- (이로 인해 개발 생산성이 높아지고 안정성이 확보됩니다.)
2. JVM의 핵심 아키텍처 (Architecture)
JVM은 크게 Class Loader, Runtime Data Area, Execution Engine의 세 부분으로 구성됩니다. 
2.1. Class Loader (클래스 로더)
자바 컴파일러(javac)가 만든 .class 파일(바이트코드)을 런타임에 메모리로 가져오는 역할을 합니다.
- 특징: 모든 클래스를 한 번에 불러오지 않고, 애플리케이션 실행 중 필요한 시점에 동적으로 로드(Dynamic Loading)합니다.
- 로딩 과정 (3단계):
- Loading (로딩): 클래스 파일을 찾아서 JVM 메모리에 올립니다.
- Bootstrap -> Extension -> Application Class Loader 순서
- Linking (링킹): 로드된 클래스를 검증하고 사용할 준비를 합니다.
- _Verify:_ 바이트코드가 유효한지 검증 (보안 위협 체크).
- _Prepare:_ 클래스 변수(static 변수)를 위한 메모리를 확보하고 기본값으로 초기화.
- _Resolve:_ 심볼릭 레퍼런스를 실제 메모리 주소(Direct Reference)로 교체.
- Initialization (초기화): static 블록을 실행하고 static 변수에 실제 값을 할당합니다.
- java.lang.Class: 클래스 로딩이 완료되면, 해당 클래스의 정보를 담은
Class객체가 힙에 생성됩니다. (우리가 리플렉션(Reflection)을 사용할 때 쓰는 그 객체입니다.)

2.2. Runtime Data Area (런타임 데이터 영역)
JVM이 OS로부터 할당받은 메모리 공간입니다.
- 공유 영역 (모든 스레드 공통):
- Method Area: 클래스 정보, static 변수, 상수 풀(Constant Pool) 등이 저장됩니다.
- (PermGen -> Metaspace로 변경됨)
- Heap Area:
new연산자로 생성된 모든 인스턴스(객체)가 저장됩니다. - GC의 주 무대이며 가장 큰 공간을 차지합니다.
- 스레드별 영역 (스레드마다 생성):
- Stack Area: 메서드 호출 시 생성되는 프레임(지역변수, 매개변수) 저장.
- PC Register: 현재 실행 중인 명령 주소 저장.
- Native Method Stack: 자바 외의 언어(C/C++) 호출 시 사용.

2.3. Execution Engine (실행 엔진)
메모리에 로드된 바이트코드를 실제로 실행하는 장치입니다. 바이트코드는 기계어가 아니므로 CPU가 이해할 수 있도록 변환이 필요합니다.
- Interpreter (인터프리터): 바이트코드를 한 줄씩 읽어서 기계어로 번역하고 실행합니다. (초기 실행 속도는 느릴 수 있음)
- JIT Compiler (Just-In-Time): 인터프리터의 단점을 보완합니다. 자주 실행되는 코드(Hot Spot)를 발견하면, 이를 통째로 네이티브 코드로 컴파일하여 캐싱해 둡니다. 이후에는 컴파일된 코드를 바로 실행하여 성능을 비약적으로 높입니다.
- Garbage Collector (GC): 힙 영역에서 더 이상 참조되지 않는 객체를 찾아 메모리를 회수합니다.

3. 심화: 객체 생성과 실행의 세부 과정
3.1. 객체 생성의 내부 메커니즘 (Heap Allocation)
코드에서 new MyClass()를 호출하면 힙 영역에서는 다음과 같은 일이 벌어집니다.
- 메모리 공간 확보: 객체를 저장할 연속된 메모리를 할당합니다. (견적서 제외, 실제 내용물 크기만큼)
- 객체 헤더 설정: 객체 관리를 위한 필수 정보를 헤더에 기록합니다.
- _Identity HashCode:_ 객체 고유의 해시값.
- _GC Age:_ GC가 몇 번이나 지나갔는지 기록 (오래 살아남은 객체 구분을 위해).
- _Lock 정보:_ 스레드 동기화를 위한 락 상태.
- 생성자(Constructor) 호출: 필드 값을 초기화합니다. (기본값보다 생성자에서 설정한 값이 우선시되는 이유)
3.2. Bytecode와 상수 풀 (Constant Pool)
.class 파일은 JVM이 읽을 수 있는 16진수 파일입니다.
- LDC (Load Constant): 바이트코드 명령어 중 하나로, "스트링 같은 상수를 상수 풀(Constant Pool)에 넣어라" 또는 "상수 풀에서 가져와라"라는 의미입니다.
- IntelliJ 같은 IDE의 "Show Bytecode" 기능을 통해 이 내용을 직접 확인할 수 있습니다.
4. 결론 및 주의사항
4.1. JNI (Java Native Interface)
기본적으로 JVM은 OS와 분리되어 있지만, JNI를 통해 Native Method Library(C/C++로 작성된 라이브러리)를 사용할 수 있습니다.
이를 통해 OS의 고유 기능을 직접 제어하거나 하드웨어 성능을 극한으로 끌어올릴 수 있습니다.
4.2. 마지막 강조: GC에 개입하지 말 것
메모 정리 과정에서 가장 중요한 원칙이 있습니다.
"코드에서 System.gc()를 명시적으로 호출하지 마세요."
- 이유 1: GC는 JVM의 고유 권한이자 정교한 알고리즘(Stop The World 최소화 등)에 의해 스케줄링됩니다.
- 이유 2: 개발자가 강제로 호출하면 전체 시스템 성능이 심각하게 저하되거나, 예측 불가능한 동작을 유발할 수 있습니다.
public class GCAntiPattern {
public void processData() {
// 1. 객체 생성 및 사용
String tempData = new String("잠시 사용하는 데이터");
System.out.println(tempData);
// 2. 사용 종료 (참조 해제)
// 변수에 null을 대입하거나, 메서드가 종료되면 알아서 수거 대상이 됩니다.
tempData = null;
// 🚫 [BAD] 명시적 GC 호출 금지
// "지금 당장 청소해!"라고 강요하는 이 코드는
// 전체 애플리케이션을 순간적으로 멈추게(Stop-The-World) 할 수 있습니다.
System.gc();
}
}
- 결론: 메모리 로딩과 해제는 JVM(Loading & GC)에게 맡기고, 개발자는 비즈니스 로직의 완성도에 집중해야 합니다.