JVM(java virtual machine)
간단하게 자바를 돌리는 프로그램 정도로 알고 시작하자.

JVM을 사용하면 자바 프로그램을 모든 플랫폼에서 제약 없이 동작하도록 할 수 있다는 점이 가장 큰 장점이다.
왜 jvm이 필요할까?
java는 os에 종속적이지 않다는 특징을 가지고 있다.
os에 종속적이지 않다는 것을 c언어와 비교를 통해 알아보자
c언어의 실행
c언어를 실행하면 컴파일러가 기계어(Binary code,기계가 읽을 수 있는 이진 코드)로 바꾸어주고 컴퓨터가 이해되게 한다.
하지만 이 기계어는 특정 OS나 CPU구조에 맞춰진 컴파일러에 의해 다르게 컴파일 된다는 특징이 있다.
==>> 다른 환경의 기기(다른 os, cpu)를 사용하는 환경에서는 이 기계어를 이해할 수 없다는 것.

이를 '이식성이 낮다' 표현하기도 한다.
다시 java로 돌아가서,
java소스 파일은 직접 OS에 가서 실행하는 것이 아니라 JVM을 거쳐서 OS와 상호작용을 하는 것이다.
즉, JVM만 있으면 OS로부터 독립적으로 프로그램을 제약없이 실행이 가능하다는 것이다.

JAVA가 실행되는 과정
1. Java Compiler가 JAVA로 작성된 소스 코드 .java 파일을 .class 파일인 Byte Code로 컴파일한다.
(단, 해당 코드는 직접 CPU에서 동작할 수 있는 코드가 아니다. 정확히 말하면 가상머신 JVM이 이해할 수 있는 코드이다)
2. 이제 이 Byte Code를 기계어로 변환시키기 위해 가상 CPU가 필요한데, 이것이 JVM(Java Virtual Machine)의 역할이다.
3. JVM이 Byte Code를 기계어(Binary Code)로 변환한다.
4. 이렇게 JVM에 의해 컴파일된 기계어는 바로 CPU에서 실행되어 사용자에게 서비스를 제공해준다.
위와 같이 OS에 종속적이지 않다는 장점을 가지고 있지만 단점도 존재한다.
두번의 컴파일로 상대적으로 속도가 느리다는 단점을 가진다.
이를 보완하기 위해 JIT 컴파일러라는 내부 프로그램을 사용해서 필요한 부분만 기계어로 바꾸어 줌으로써 성능향상을 가져오도록 했지만 c언어 실행속도를 따라잡지 못했다.
JIT(just - in -time) 컴파일러란?
기존 자바는 인터프리터 방식으로 명령어를 하나씩 실행하게끔 이루어져 실행속도가 느렸다.
JIT 컴파일러는 같은 코드를 매번 해석하지 않고, 실행할때 컴파일하며 해당 코드를 캐싱한다. 이후엔 바뀐 부분만 컴파일하고 나머지 캐싱된 코드를 사용한다.

자바 프로그램의 실행과정

1. 소스 코드를 작성. (.java 파일)
2. 컴파일러는 자바 소스 코드를 이용해 클래스 파일(.class)을 생성. 컴파일된 클래스 파일은 JVM이 인식할 수 있는 바이트 코드 파일임.
3.JVM은 클래스파일의 바이트 코드를 해석해 바이너리 코드로 변환하고 프로그램을 수행한다.
4. myprogram 수행결과가 컴퓨터에 반영된다.
간단한 실습!
public class MyProgram {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
위처럼 .java파일을 만든다.
window기준으로, cmd창을 열고 .java파일이 있는 곳으로 이동한다.
해당 위치에서 javac명령어로 컴파일을 진행한다.

현재 위치에 .class파일이 생성되고 이제 java 명령어로 class파일을 실행시키면 코드의 결과를 얻을 수 얻게된다.
이 java명령어가 JVM을 실행시키는 것이다.
> java MyProgram # 파일을 실행시킬때 .class 확장자는 뺀다.
Hello World
바로 위에서 java 명령어가 클래스 파일의 바이트 코드를 바이너리 코드로 바꾸는 과정으로 프로그램이 실행된다고 적어두었다.
그 바꾸는 과정에서 JVM을 실행시키는 것인데 더 자세히 살펴보자.
JVM의 동작 방식
JVM의 역할은 자바 애플리케이션을 클래스 로더를 통해 읽어 자바 API와 함께 실행하는 것.

1. 자바 프로그램을 실행하면 JVM은 OS로부터 메모리를 할당 받는다. (OS와는 종속적이다!)
2. 자바 컴파일러(javac)가 자바 소스 코드를(.java)를 자바 클래스 파일(=바이트코드)로 컴파일함.
// 여기까지 우리가 위에서 공부한 내용.
3. class loder는 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크해 Runtime Data Area(실질적 메모리를 할당받아 관리하는 영역)에 올린다.
4. Runtime Data Area에 로딩된 바이트 코드는 Exeution Engine을 통해 해석된다.
5. 이 과정에서 Exeution engine에 의해 Garbage Collector의 작동과 Thread동기화가 이루어짐.
추후, 자세한 설명이 있을 예정.
JVM의 구조 (더 상세하게)

하나씩 뜯어보자.
Class Loder

클래스 로더는 JVM 내로 클래스파일(.class)을 동적으로 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈임.
=> 로드된 바이트 코드(.class)들을 엮어서 JVM의 메모리 영역인 Runtime Data Area에 배치한다.
클래스를 메모리에 올리는 로딩 기능은 한번에 메모리에 올리지 않고 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다.

1. Loading: 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
2. Linking : 클래스 파일을 사용하기 위해 검증하는 과정이다.
2-1. verifying(검증) : 읽어들인 클래스가 JVM 명세에 명시된대로 구성되어있는지 검사한다.
2-2 preparing: 클래스가 필요로 하는 메모리 할당
2-3. resolving(분석) : 클래스의 상수 풀 내 모든 심
3. Initialization (초기화): 클래스 변수들을 적절한 값으로 초기화한다.( static 필드들을 설정된 값으로 초기화 등...)
Execution Engine( 실행 엔진)
실행 엔진은 class loder을 통해 runtime data area에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.
자바 바이트 코드(.class = 클래스 파일)은 기계가 바로 수행할 수 있는 언어가 아니다. 가상머신(=JVM)이 이해할 수 있는 중간 레벨로 컴파일된 코드이다. 그래서 Execution Engine은 바이트 코드를 기계가 실행할 수 있는 형태로 변경해준다.
인터프리터와 JIT 컴파일러 두가지 방식을 혼합해 바이트 코드 실행.

인터프리터(기본값으로 실행된다.)
바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행한다.
다만, 같은 메소드라도 여러번 호출이 된다면 매번 해석하고 수행해야 되서 전체적 속도가 느리다.
JIT 컴파일러
인터프리터 단점을 보완하기 위해 도입된 방식. 반복되는 코드를 발견해 바이트 코드 전체를 컴파일하여 Native code로 변경하고 이후 해당 메서드를 더 이상 인터프리팅 하지 않고 캐싱해두었다가 native code로 직접 실행하는 방식.
컴파일된 native code를 실행하는 것이라 인터프리터 방식보다 훨 빠름.
단, 바이트 코드를 native code로 변환하는데도 비용이 소모되므로 인터프리터 방식을 사용하다 일정 수준이 넘어가면 JIT컴파일러 방식으로 명령어를 실행함.
@Native code : java에서 부모가 되는 c언어나, c++, 어셈블리어 로 구성된 코드를 말함.
Garbage collector(가비지 컬렉터, GC)

JVM은 GC를 이용해 Heap 메모리 영역에서 더는 사용하지 않는 메모리를 자동으로 회수해준다.
c언어의 경우 직접 개발자가 메모리를 해제시켜야 하지만, Java는 GC를 이요해 자동으로 메모리를 실시간으로 최적화 시켜준다.
GC가 실행되는 시간은 정해져있지 않다.
더 자세한 내용은 다른 글로 작성하겠다.
Runtime data Area

간단히 말해, JVM의 메모리 영역이다. 자바 애플리케이션을 실행할때 사용되는 데이터들을 적재하는 영역이다.

여기서 method area, heap area는 모든 쓰레드가 공유하는 영역이다.
나머지 Stack area, pc register, native method stack은 각 쓰레드마다 생성되는 개별 영역이다.
아래 그림을 보면 이해가 편하다.

Method Area (메서드 영역) = class area나 static area라고도 부름
메서드 영역은 JVM이 시작될때 생성되는 공간이다.
바이트 코드(.class)를 처음 메모리 공간에 올릴때 초기화 되는 대상을 저장하기 위한 메모리 공간이다.
JVM이 동작하고 클래스가 로드될 때 적재되서 프로그램이 종료될 때까지 저장된다.
모든 쓰레드가 공유하는 영역이라 다음과 같이 초기화 코드 정보들이 저장되게 된다.
1. Field Info : 멤버 변수의 이름, 데이터 타입, 접근 제어자의 정보
2. Method Info : 메소드 이름, return 타입, 함수 매개변수, 접근 제어자의 정보
3. Type Info: class인지 interface인지 여부 저장, Type의 속성, 이름 Super class의 이름.

@Runtime Constant Pool

1. 메서드 영역에 존재하는 별도의 관리영역
2. 각 클래스/인터페이스 마다 별도의 constant pool 테이블이 존재하는데, 클래스 생성할때 참조해야 할 정보들을 상수로 가지고 있는 영역이다.
3. JVM은 이 constant pool을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조한다.
=> 정리하면 상수 자료형을 저장하여 참조하고 중복을 막는 역할이다.
Heap area
heap area도 method area와 같이 모든 쓰레드가 공유한다.
데이터를 저장하기 위해 런타임시 동적으로 할당하여 사용하는 영역이다.
즉, new 연산자로 생성되는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳이다.
당연히, method area영역에 저장된 클래스만이 생성되어 적재된다.
- 힙 영역의 사용 기간 및 스레드 공유 범위
1. 객체가 더이상 사용되지 않거나 명시적으로 null 선언시
2. GC 대상.

유의할 점은 힙 영역에서 생성된 객체와 배열은 Reference Type으로, JVM 스택영역의 변수나 다른 객체 필드에서 참조된다는 점이다.
즉, 힙의 참조 주소는 "스택"이 갖고 있고 해당 객체를 통해서만 힙 영역에 있는 인스턴스를 핸들링 할 수 있는 것.

만일 참조하는 변수나 필드가 없다면 의미 없는 객체가 되기 때문에 이것을 쓰레기 취금하여 JVM은 GC를 실행시켜 쓰레기 객체를 자동으로 힙에서 제거한다.
힙은 GC의 대상이 되는 공간이다. 그리고 효율적인 GC를 위해 세부적으로 5가지 영역으로 나뉜다.

5가지 영역은 다시 물리적으로 Young Generation 과 Old Generation 영역으로 구분되는데 다음과 같다.
Young Generation : 생명주기가 짧은 객체를 GC대상으로 하는 영역
-Eden : new 를 통해 새로 생성된 객체가 위치함. 정기적인 쓰레기 수집 후 살아남은 객체들은 Survivor로 이동한다.
- Survivor 0/ Survivor 1: 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 순차적으로 이동한다.
Old Generation : 생명주기가 긴 캑체를 GC 대상으로 하는 여역, Young에서 살아남은 객체가 이동한다.
자세한 동작은 다른 글에서 설명.
Stack Area
int, long, boolean등 기본 자료형을 생성할때 저장하는 공간, 임시적으로 사용되는 변수나 정보들이 저장되는 영역이다.

메서드 호출 시마다 각각의 stack frame이 생성(그 메서드만을 위한 공간)되고 메스드 안에서 사용되는 값들을 저장하고, 호출된 메스드의 매겨변수, 지역변수, 리턴 값 및 연산시 일어나는 값들을 임시로 저장한다.
그리고 메서드 수행이 끝나면 프레임별로 삭제된다.
단, 데이터의 타입에 따라 Stack과 heap에 저장되는 방식이 다르다는 것을 유의해야 한다. // 아까 위에서 말한 내용
1. 기본(원시) 타입 변수는 스택 영역에서 직접 값을 가진다.
2. Reference type 변수는 heap이나 method area의 객체 주소를 가진다.
예>

new 에 의해 생성된 클래스는 Heap area에 저장되고, Stack Area에는 생성된 클래스의 참조인 p만 저장된다.
stack 영역은 각 스레드마다 하나씩 존재하며, 스레드가 시작될때 할당된다.
프로세스가 메모리에 로드될때 스택 사이즈가 고정되어있어, 런타임 시에 스택 사이즈를 바꿀 수 없다.
쓰레드를 종료하면 런타임 스택도 사라진다.
위의 메서드 영역, 힙 영역, 스택영역을 정리한 그림이다.

PC 레지스터(Program counter Register)
pc 레지스터는 쓰레드가 시작될때 생성됨.
현재 수행중인 JVM 명령어 주소를 저장하는 공간이다.
JVM 명령의 주소는 쓰레드가 어떤 부분을 무슨 명령으로 실행해야 할지에 대한 기록을 가지고 있다.
일반적인 CPU연산 과정과 레지스터
일반적으로 프로그램의 실행은 CPU에서 명령어(Instruction)을 수행하는 과정으로 이루어진다.
이때 CPU는 연산을 수행하는 동안 필요한 정보를 레지스터라고 하는 CPU 내의 기억장치를 이용하게 된다.
예를들어, A와 B라는 데이터와 피연산 값인 Operand가 있고 이를 더하라는 연산 Instruction이 있다고 하자.
A와 B, 그리고 더하라는 연산이 순차적으로 진행이 되게 되는데, 이때 A를 받고 B를 받는 동안 이 값을 CPU가 어딘가에 기억해 두어야 할 필요가 생긴다.
이 공간이 바로 CPU 내의 기억장치 Register이다.
하지만 자바의 PC Register는 위의 cpu Register와 다르다.
자바는 OS나 CPU의 입장에서는 하나의 프로세스이기 때문에 가상 머신(JVM)의 리소스를 이용해야 한다.
그래서 자바는 CPU에 직접 연산을 수행하도록 하는 것이 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며, 이를 위한 버퍼 공간으로 PC Register라는 메모리 영역을 만들게 된 것이다
따라서 JVM은 스택에서 비연산값 Operand를 뽑아 별도의 메모리 공간인 PC Register에 저장하는 방식을 취한다.

Native Method Stack
자바 코드가 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다.
또한 자바 이외의 언어(c, c++, 어셈블리 등)로 작성된 네이티브 코드를 실행하기 위한 공간이기도 함.
JIT 컴파일러에 의해 변환된 native code 역시 여기에서 실행이 된다고 보면된다.

일반적으로 메소드를 실행하는 경우 JVM 스택에 쌓이다가 해당 메소드 내부에 네이티브 방식을 사용하는 메소드가 있다면 해당 메소드는 네이티브 스택에 쌓인다.
그리고 네이티브 메소드가 수행이 끝나면 다시 자바 스택으로 돌아와 다시 작업을 수행한다.
그래서 네이티브 코드로 되어 있는 함수의 호출을 자바 프로그램 내에서도 직접 수행할 수 있고 그 결과를 받아올 수도 있는 것이다.
JNI가 사용되면 Native method stack에 바이트 코드로 전환되어 저장되게 한다.
@JNI(java native interface)
JNI는 자바가 다른 언어로 만들어진 어플리케이션과 상호 작용할 수 있는 인터페이스 제공하는 프로그램이다.
위에서 다뤄봤듯이, JNI는 JVM이 Native Method를 적재하고 수행할수 있도로 한다.
하지만 실질적으로 제대로 동작하는 언어는 C / C++ 정도 밖에 없다고 한다.

Native Method Library
C, C++로 작성된 라이브러리를 칭한다.
만일 헤더가 필요하면 JNI는 이 라이브 러리를 로딩해 실행한다.

☕ JVM 내부 구조 & 메모리 영역 💯 총정리
저번 포스팅에서는 JRE / JDK / JVM에 대해서 간략하게 알아보는 시간을 가졌다면, 이번 포스팅에서는 JVM의 내부 구조에 대해 좀 더 자세하게 알아보도록 할 예정이다. JVM(자바 가상 머신)은 자바 언
inpa.tistory.com
공부용으로 작성한 글입니다. 문제시 비공개처리하겠습니다.
'언어 > java' 카테고리의 다른 글
| 객체의 메모리 레이아웃과 Heap(JVM) (1) | 2026.01.23 |
|---|---|
| Runtime Data Areas(JVM)이란? (0) | 2026.01.21 |
| JVM의 Class Loader(클래스 로더)란? (0) | 2026.01.19 |