-
[Java] JVM이 무엇이고 Java 코드는 어떻게 동작할까?Java 2025. 11. 18. 20:39

1. JVM이란?
JVM(Java Virtual Machine)은 자바 프로그램을 실행시켜주는 가상 머신 입니다.
JVM의 핵심 철학은 "Write Once, Run Anywhere" 입니다. 한 번 작성하면 어디서든 동일하게 실행된다는 것을 의미합니다.
어떻게 그렇게 되는지는 뒤에서 확인하겠습니다.
2. JVM의 주요 기능
JVM은 단순히 바이트 번역만 하는 것이 아니라 다음과 같은 중요한 일들을 자동으로 처리합니다.
- 바이트 코드 실행: .class 파일을 읽어 들여(로드) 메모리에 적재하고 실행합니다.
- 플랫폼 독립성 보장: 위에서 설명한 'Write Once, Run Anywhere'를 실현합니다.
- 메모리 관리 (Garbage Collection):
- 개발자가 C/C++처럼 직접 메모리를 할당하고 해제할 필요가 없습니다.
- JVM의 '가비지 컬렉터(Garbage Collector)'가 프로그램에서 더 이상 사용하지 않는 객체(메모리)를 자동으로 찾아 정리해줍니다.
- 이 덕분에 개발자는 메모리 누수(Memory Leak) 걱정을 덜고 비즈니스 로직 개발에 집중할 수 있습니다.
- JIT 컴파일 (Just-In-Time Compilation):
- 프로그램 실행 속도를 높이기 위한 기술입니다.
- 처음에는 코드를 한 줄 한 줄 해석(Interpreting)하다가, 자주 사용되는 코드가 발견되면 해당 부분을 런타임(실행 중)에 기계어로 미리 컴파일해 둡니다.
- 덕분에 다음부터는 번역 없이 컴파일된 코드를 바로 실행하여 속도가 매우 빨라집니다.
3. Java 코드 동작 과정

Java 코드 동작 과정 1단계: 코드 작성
// MyProgram.java public class MyProgram { public static void main(String[] args) { System.out.println("Hello, JVM!"); } }2단계: 컴파일 (.java -> .class)
이 단계는 JVM이 실행되기 전 '컴파일 타임'에 일어납니다.
컴파일 (javac MyProgram.java):
- 문법 검사: 개발자가 작성한 코드에 문법 오류가 없는지 확인합니다.
- 바이트 코드 생성: 문법에 이상이 없다면, JVM이 이해할 수 있는 범용 언어인 자바 바이트 코드 (.class 파일)를 생성합니다.
3단계: 클래스 로딩 (JVM의 Class Loader)
이 단계에서는 JVM의 클래스 로더 서브 시스템(Class Loader Subsystem)이 실행할 .class 파일(바이트 코드)을 디스크에서 읽어와, JVM의 메모리 영역(Runtime Data Area)에 적재하여 실행할 수 있도록 준비합니다.
이 단계는 '런타임', 즉 java MyProgram 명령으로 JVM이 시작된 직후에 일어나며 다음과 같이 3개의 작은 단계로 나뉩니다.
1. 로딩 (Loading)
- 역할: .class 파일(바이트 코드)을 디스크 또는 네트워크에서 찾아 JVM 메모리의 '메소드 영역(Method Area)'에 적재하는 단계입니다.
- 동작: 위임 계층 모델 (Parent Delegation Model)위임 계층 모델은 JVM의 클래스 로더가 클래스를 찾는 안정적이고 계층적인 방식을 말합니다.
- 계층 구조(Java 9+ 기준):
- Bootstrap ClassLoader(최상위)
- 가장 기본이자 최상위 로더이며, 모든 클래스 로더의 “루트(Root)” 입니다. 자바 9 모듈 시스템을 도입한 이후, java.base 를 포함한 소수의 핵심 모듈들만 로드합니다. 이 모듈에는 자바 실행에 없어서는 안 될 가장 필수적인 핵심 클래스들이 포함됩니다. 예시에는 java.lang.Object, java.lang.String, java.lang.Thread, java.util.List 등이 있습니다. 네이티브 코드(C++)로 구현되어 있다는 특징이 있습니다.
- Platform ClassLoader (부모)
- Bootstrap의 자식이며, Application의 부모가 되는 "중간 관리자"입니다. Java 9부터 Extension ClassLoader가 Platform ClassLoader로 이름이 변경되었으며, lib/ext 폴더 개념이 사라졌습니다. 대신 java.base 모듈을 제외한 모든 '자바 표준 라이브러리 모듈'을 로드합니다. 예시에는 java.sql (데이터베이스 접속 API 모듈), java.management (JVM 모니터링 API 모듈), java.xml (XML 파싱 API 모듈), java.desktop (GUI 관련 AWT/Swing API 모듈) 등이 잇습니다.
- Application ClassLoader (자식)
- "System ClassLoader"라고도 불립니다. 개발자가 작성한 클래스(MyProgram.class)나 외부 라이브러리(.jar)처럼 사용자의 클래스패스(Classpath)에 있는 클래스들을 로드합니다.
- 작동 방식 (위임 우선):
- Application 로더가 MyProgram.class 로드 요청을 받습니다.
- Application 로더는 먼저 로드하지 않고, 부모인 Platform 로더에게 요청을 위임합니다.
- Platform 로더 역시 먼저 로드하지 않고, 부모인 Bootstrap 로더에게 요청을 위임합니다.
- 최상위 Bootstrap 로더가 "이 클래스가 java.base 모듈에 속하는가?"를 먼저 검색합니다.
- Bootstrap이 찾지 못하면(MyProgram.class는 핵심 모듈이 아님), 요청이 Platform 로더로 돌아옵니다.
- Platform 로더가 "이 클래스가 표준 라이브러리 모듈에 속하는가?"를 검색합니다.
- Platform이 찾지 못하면, 요청이 마침내 Application 로더로 돌아옵니다.
- Application 로더가 클래스패스에서 MyProgram.class를 검색하고, 찾으면 디스크에서 읽어 메모리에 로드합니다.
- 효과
- 중복 로딩 방지 (안정성):java.lang.Object 같은 자바의 핵심 클래스는 어떤 로더가 요청하든 항상 최상위 Bootstrap 로더가 단 한 번만 로드하게 됩니다. 이로써 JVM 내에 동일한 클래스가 여러 개 로드되어 발생하는 혼란을 막습니다.
- 보안 (Security): 만약 개발자가 악의적으로 java.lang.String이라는 이름의 클래스를 만들어도, Bootstrap 로더가 항상 java.base 모듈의 '진짜' String 클래스를 먼저 로드합니다. 덕분에 사용자가 만든 가짜 클래스가 핵심 클래스를 대체하는 것을 원천적으로 차단합니다.
- 계층 구조(Java 9+ 기준):
- 클래스 로드 요청을 받으면, 하위 클래스 로더에게 먼저 요청을 보내지 않고 상위 클래스 로더에게 먼저 요청을 보내게 됩니다.
- 단순히 클래스를 찾는 것이 아니라, JVM은 '위임 계층 모델'이라는 안정적인 방식을 사용합니다.
- 결과: 설계도(Method Area)와 객체(Heap)의 분리
1. 메소드 영역 (Method Area)
-
- 저장되는 것: 클래스의 '설계도 원본' 데이터가 저장됩니다.
- 상세 내용:
- 타입 정보: 클래스의 전체 이름, 부모 클래스 이름, 인터페이스 목록 등
- 필드 정보: 멤버 변수(필드)의 이름, 타입, 접근 제어자 정보
- 메소드 정보: 메소드의 이름, 리턴 타입, 파라미터, 접근 제어자 정보
- 메소드 바이트 코드: 메소드가 실제로 실행할 바이트 코드
- static 변수: static으로 선언된 클래스 변수
2. 힙 영역 (Heap)
-
- 저장되는 것: 클래스 로딩이 완료되면, 해당 클래스를 프로그램 내에서 '관리'하고 '참조'하기 위한 java.lang.Class 타입의 객체(인스턴스)가 힙(Heap) 영역에 단 하나 생성됩니다.
- 상세 내용:
- 참조 역할: 이 객체는 '메소드 영역(Method Area)'에 저장된 클래스의 실제 데이터('설계도 원본': 바이트 코드, 필드, 메소드 정보 등)를 참조하는 주소값을 가지고 있습니다.
- 프로그램의 접점: 자바 프로그램이 리플렉션(Reflection) 등을 통해 클래스에 대한 정보를 파악할 때, JVM은 바로 이 힙(Heap)에 있는 Class 객체를 통해 '메소드 영역'의 원본 정보를 조회합니다.
- 식별: 코드에서 MyProgram.class라고 쓰거나, 객체 obj에 대해 obj.getClass()를 호출할 때 반환되는 것이 바로 이 힙(Heap)에 있는 Class 객체입니다.
- 로딩이 성공적으로 완료되면, 클래스 데이터는 두 곳의 메모리 영역에 나뉘어 저장됩니다.
2. 연결 (Linking)
'연결'은 로드된 클래스(바이트 코드)를 JVM의 실행 엔진이 실제로 실행할 수 있는 상태로 만드는 과정입니다.
이 단계는 다시 3개의 작은 단계로 나뉩니다.
- 검증 (Verification)
- 역할: 로드된 .class 파일이 유효하고 안전한지 검사하는 단계입니다.
- 동작:
- 파일 형식이 JVM 명세에 맞는지 확인합니다.
- 바이트 코드가 유효한 작업(예시: 스택 오버플로우를 일으키는 코드인지)을 하는지 검사합니다.
- 타입(유형)이 올바른지 확인합니다. (예시: String 변수에 Integer를 할당하려 하지 않는지)
- 이유: 컴파일러가 아닌, 조작된 바이트 코드가 JVM을 손상시키는 것을 방지하기 위한 필수 보안 조치입니다.
- 준비 (Preparation)
- 역할: 클래스가 사용하는 '클래스 변수'(static 변수)를 위한 메모리를 '메소드 영역(Method Area)'에 할당하는 단계입니다.
- 동작:
- 핵심: 이때는 개발자가 지정한 초기값이 아닌, 자료형의 기본값으로 먼저 초기화됩니다.
- int / long: 0
- boolean: false
- float / double: 0.0
- 참조 타입 (객체): null
- 핵심: 이때는 개발자가 지정한 초기값이 아닌, 자료형의 기본값으로 먼저 초기화됩니다.
- 예시: 코드에 private static int myValue = 100;라고 되어 있어도,
- '준비' 단계에서는 myValue를 위한 공간(4바이트)을 확보하고 기본값인 0으로 설정합니다.
- 분석 (Resolution)
- 역할: 바이트 코드 내의 '기호적 참조(Symbolic Reference)'를 '직접 참조(Direct Reference)'로 바꾸는 과정입니다.
- 설명:
- 기호적 참조 (이름): 컴파일된 .class 파일에는 "String 클래스의 'println' 메소드를 찾아라"처럼 이름으로만 기록되어 있습니다.
- 직접 참조 (주소): '분석' 단계에서 JVM은 '메소드 영역'에 실제로 로드된 String 클래스를 찾아, 'println' 메소드가 위치한 실제 메모리 주소(e.g., 0x123ABC)로 이 참조를 바꾸어 둡니다.
- 이유: 실행 엔진(Execution Engine)은 '이름'을 보고 찾는 것이 아니라, '주소'를 보고 바로 실행해야 빠르기 때문입니다.
3. 초기화 (Initialization)
- 역할: '준비' 단계에서 기본값으로 설정했던 static 변수들에 개발자가 지정한 실제 값을 할당합니다.
- 동작:
- 클래스의 static 블록 (static { ... })이 있다면 이 코드가 실행됩니다.
- static 변수의 할당 구문이 실행됩니다.
- 예: '준비' 단계에서 0이었던 myValue가,
- '초기화' 단계에서 드디어 100이라는 값을 갖게 됩니다.
4단계: 바이트 코드 실행 (JVM의 Execution Engine)
클래스 로딩이 모두 끝나면, '실행 엔진(Execution Engine)'이 '메소드 영역'에 있는 바이트 코드를 가져와 실행합니다. 이때 JVM의 '런타임 데이터 영역(Runtime Data Areas)'이 작업 공간으로 사용됩니다.
- 메소드 호출: main 메소드(혹은 다른 메소드)가 호출됩니다.
- 스택 프레임 생성:
- JVM은 'JVM 스택(Stack)' 영역에 해당 메소드만을 위한 '스택 프레임(Stack Frame)'이라는 작업 공간을 만듭니다. (이 스택은 스레드마다 하나씩 생성됩니다.)
- 이 '스택 프레임' 안에는 이 메소드가 사용하는 '지역 변수'(main 메소드의 args 등), 계산을 위한 임시 공간('피연산자 스택') 등이 포함됩니다.
스텍 프레임이란?
스텍 프레임(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) 에 대한 참조입니다. 메소드에서 System.out.println 같은 다른 메소드를 호출할 때, 이 참조를 통해 실제 메모리 주소를 찾아갑니다.
메소드 리턴 주소(Return Address)는 메소드가 종료된 후, 돌아가야 할 호출 지점(PC Register 값)을 저장합니다.3. 코드 실행 (Execution)
클래스 로딩이 끝나면 실행 엔진(Execution Engine)이 본격적으로 바이트 코드를 실행합니다. 이 과정은 인터프리터, JIT 컴파일러, 그리고 JNI(Native Interface)을 통해 이루어집니다.
① 인터프리터 (Interpreter) 프로그램 시작 직후에는 인터프리터가 코드를 실행합니다.
- Fetch (가져오기): PC 레지스터가 가리키는 메모리 주소(메소드 영역)에서 실행할 바이트 코드 명령어(OpCode) 하나를 읽어옵니다.
- Decode (해석): 해당 명령어가 무엇을 의미하는지 해석합니다. (예: iload_1 → "지역 변수 1번을 스택으로 불러와라")
- Convert (변환): 해석된 명령을 현재 CPU와 운영체제가 이해할 수 있는 기계어로 즉시 변환합니다.
- Execute (실행): 변환된 기계어를 CPU가 실행합니다. 이 과정에서 스택 프레임의 데이터가 변경되거나 힙 영역에 객체가 생성됩니다.
- Discard (폐기): 실행된 기계어 코드는 캐싱되지 않고 버려집니다. 만약 반복문에 의해 같은 코드를 다시 만나더라도, 처음부터 다시 해석(Decode & Convert)해야 합니다.
② JIT 컴파일러 (Just-In-Time) 인터프리터가 실행하는 동안, JIT 컴파일러는 백그라운드에서 '프로파일링(Profiling)'을 수행합니다.
- HotSpot 감지: 특정 메소드나 루프가 실행될 때마다 내부 카운터를 증가시킵니다. 이 카운터가 임계치(Threshold)를 넘으면 해당 코드를 '핫스팟(HotSpot)'으로 간주합니다.
- Compile (통째로 번역): JIT 컴파일러는 핫스팟으로 지정된 바이트 코드 전체를 가져와 네이티브 기계어(Native Code)로 컴파일합니다.
- Optimize (최적화): 단순히 번역만 하는 것이 아니라, 불필요한 코드를 제거하거나 메소드를 인라인(Inlining) 하는 등 강력한 성능 최적화를 수행합니다.
- Caching (코드 캐시 저장): 컴파일된 네이티브 기계어 코드는 '코드 캐시(Code Cache)'라는 별도의 메모리 영역에 저장됩니다. (인터프리터와 달리 사라지지 않습니다.)
- Execute (교체 실행): 이후 해당 메소드가 호출되면, 인터프리터의 해석 과정을 건너뛰고 코드 캐시에 저장된 기계어를 바로 실행하여 실행 속도를 비약적으로 높입니다.
③ 네이티브 메소드 실행 (JNI & Native Library) 자바 코드에서 native 키워드가 붙은 메소드(예: Thread.start(), 파일 I/O 등)를 실행할 때의 과정입니다.
- Native Method Stack 전환: native 메소드가 호출되면, JVM은 일반 'JVM 스택'에서 실행을 멈추고 '네이티브 메소드 스택(Native Method Stack)'으로 작업 공간을 전환합니다.
- JNI Lookup: 실행 엔진은 JNI(Java Native Interface)를 통해 해당 메소드와 연결된 네이티브 라이브러리(C/C++로 작성된 .dll, .so 파일) 내의 함수를 찾습니다.
- Native Execution:
- 이때는 바이트 코드를 해석하는 것이 아닙니다.
- 이미 C/C++로 컴파일되어 있는 순수 기계어 코드(Native Code)가 CPU로 직접 전달되어 실행됩니다.
- Result Conversion: 네이티브 코드 실행이 끝나면, JNI가 결과값을 자바 데이터 타입으로 변환하여 반환하고, 실행 제어권이 다시 'JVM 스택'으로 넘어옵니다.
4. 메소드 종료
- 메소드의 모든 코드가 실행되거나 return 문을 만나면, 해당 메소드의 '스택 프레임'이 'JVM 스택'에서 제거(Pop)됩니다.
- 이전 스택 프레임(해당 메소드를 호출한 곳)으로 돌아가며, main 메소드까지 종료되면 프로그램 전체가 종료됩니다.
5단계: 자원 회수 (Garbage Collector)
- 프로그램이 실행되는 동안 '힙(Heap) 영역'에는 수많은 객체들이 생성됩니다.
- '가비지 컬렉터(Garbage Collector, GC)'가 주기적으로 '힙' 영역을 감시합니다.
- '스택' 영역 등에서 더 이상 참조하지 않는 객체(즉, '쓰레기'가 된 객체)를 찾아내어 메모리에서 자동으로 해제(삭제)합니다
Garbage Collector에 대한 내용은 추후에 더 자세히 알아보겠습니다.
Java 코드 동작 과정 요약
- 컴파일 (Compile): 개발자가 .java 코드를 작성하면, 자바 컴파일러(javac)가 이를 .class(바이트 코드) 파일로 변환합니다.
- 로딩 (Load): java 명령어 실행 시, 클래스 로더가 .class 파일을 JVM 메모리(Method Area)로 불러옵니다.
- 연결 & 초기화 (Link & Init): JVM이 바이트 코드를 검증하고, static 변수 메모리를 할당합니다. (이때 0으로 초기화했다가, 바로 개발자가 지정한 실제 값으로 덮어씁니다.)
- 실행 (Execute): 실행 엔진이 코드를 실행합니다.
- Stack: 메소드가 호출될 때마다 '스택 프레임'이 쌓이는 작업 공간입니다. (지역 변수 저장)
- Heap: new로 생성된 모든 객체(인스턴스)가 저장되는 공간입니다.
- 자원 회수 (GC): 가비지 컬렉터(GC)가 Heap 영역에서 더 이상 사용되지 않는 객체(쓰레기)를 자동으로 찾아 메모리에서 제거합니다.
이어지는 다음 글 읽기
👉 JVM의 Runtime Data Area'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의 Runtime Data Area (0) 2025.11.19 SOLID 원칙 완벽 정리 (3) 2025.06.18