📌 JVM
Java Virtual Machine
Java는 OS에 종속적이지 않는 특징이 있는데 이는 JVM이 존재하기 때문이다.
OS에 종속받지 않고 CPU가 Java를 실행할 수 있게 된다. 결국 JVM은 JAVA와 OS 사이의 중개자 역할이다.
우리가 작성하는 파일은 .java 파일이다. 이는 우리가 보기 편하게 작성되어 있으므로 CPU가 인식하지 못한다. 기계어로 컴파일된 파일이 필요하다.
하지만 Java는 JVM을 거쳐서 OS에 도달하기 때문에 바로 기계어로 컴파일되는 것이 아니라, JVM이 인식할 수 있는 .class 파일로 변환된다.
이 때 .java → .class 로 변환하는 과정은 Java Compliler가 변환시킨다. Java Compiler는 JDK를 설치하면 bin에 존재하는 javac.exe를 말한다.
변환된 .class 는 기계어가 아니기 때문에 OS에서 바로 실행되지 않는다. 이때 JVM이 .class 를 OS가 읽을 수 있도록 도와준다. 결국 .class 는 JVM을 통해 OS 상관없이 실행될 수 있다.
- .class : 바이트 코드
- 기계어 : 이진 코드
.class 파일은 인터프리터 또는 JIT 컴파일러에 의해서 이진 코드로 변환된다.
JVM 구성 요소
JVM은 크게 3가지로 나눌 수 있다.
- 클래스 로더
- 실행 엔진
- 런타임 데이터 영역

클래스 로더
JVM으로 .class 파일을 로드하고 링크를 통해 배치하는 작업을 수행한다. 즉, 로드된 .class 파일들을 JVM 메모리 영역인 런타임 데이터 영역에 배치한다. 클래스를 처음 참조할 때, 해당 클래스를 로드하고 링크하는데 다음 과정을 거친다.
- 로딩 : .class 파일을 JVM 메모리에 로드한다.
- 링크 : .class 파일을 사용하기 위해 검증하는 과정
- 검증 : 읽은 클래스가 JVM 명세에 명시된 대로 구성되어 있는지 검사
- 준비 : 클래스가 필요로 하는 메모리 할당
- 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경
- 초기화
- 클래스 변수들을 적절한 값으로 초기화 ⇒ static 필드
❓ 심볼릭 레퍼런스? 다이렉트 레퍼런스?
심볼릭 레퍼런스 : 프로그램 코드 안에서 사용되는 이름이나 문자열 같은 것
다이렉트 레퍼런스 : 실제 메모리 주소
MyClass라는 클래스의 myMethod라는 메서드를 호출하는 코드가 있다고 가정해 보자.
myObject.myMethod()가 실행될 때 JVM은 MyClass가 어떤 클래스인지, myMethod가 어떤 메서드인지 알아야 한다.
JVM이 이 코드를 실행하기 위해 MyClass를 로드하고 링크할 때 MyClass의 심볼릭 레퍼런스를 실제 메모리 주소로 변환한다.
실행 엔진
클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.
바이트 코드는 OS가 읽을 수 있는 레벨이 아니다. 이때 실행 엔진은 바이트 코드를 실제로 JVM 내부에서 기계어로 변경해준다.
- 인터프리터
- 바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행
- 같은 메서드라도 여러 번 호출 시 매번 해석
- 전체적인 속도 느림
- JIT 컴파일러
- 반복된 코드의 바이트 코드 전체를 컴파일하여 Native Code로 변경 ⇒ 캐싱해서 Native Code로 실행
- 인터프리터보다 대체적으로 빠르다.
- But Native Code로 변환 비용 존재, 일정 기준이 넘어가면 JIT 컴파일
가바지 컬렉터
JVM은 가바지 컬렉터를 이용하여 Heap 메모리 영역에서 사용하지 않는 메모리를 자동으로 회수한다.
가비지 컬렉터에 대한 자세한 내용은 뒷 내용을 참고하자.
런타임 데이터 영역
런타임 데이터 영역은 JVM 메모리 영역으로 자바 애플리케이션을 실행할 때 사용하는 데이터를 담아두는 영역이다.
런타임 데이터 영역은 5개로 나눌 수 있다.

- PC Register
- Thread가 시작될 때 생성
- 현재 수행 중인 JVM 명령어 주소 저장
- JVM Stack
- 임시적으로 사용되는 변수나 정보들이 저장되는 영역
- int, long, boolean 등 기본 자료형을 생성할 때 저장하는 공간
- 메서드 호출 시 각각의 스택 프레임이 생성되고 메서드 안에서 사용되는 값들을 저장한다.
- 메서드 수행이 끝나면 프레임별로 삭제
- Native Method Stack
- 자바 코드가 컴파일된 바이트 코드가 아니라 실제 기계어로 작성된 프로그램 실행 영역
- JAVA가 아닌 다른 언어로 작성된 네이티브 코드 실행
- Method Area (= Class Area = Static Area)
- 바이트 코드를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 공간
- 정적 필드와 클래스 구조만 가지고 있음
- 모든 스레드가 공유하는 영역
- Field Info : 멤버 변수의 이름, 데이터 타입, 접근 제어자
- Method Info : 메서드 이름, return 타입, 함수 매개변수, 접근 제어자
- Type Info : Class/Interface 여부, Type 속성, Super Class 이름
- Runtime Constant Pool
- 각 클래스/인터페이스마다 별도의 Constant Pool이 존재하는데, 클래스 생성 시 참조해야 할 정보들을 상수로 가지고 있는 영역
- JVM은 Constant Pool을 통해 해당 메서드나 필드의 실제 메모리 상 주소를 찾아 참조
- 상수 자료형을 저장하여 참조하고 중복을 막는 역할
- Heap
- 모든 스레드가 공유
- JVM이 관리하는 프로그램 상 데이터를 저장하기 위해 런타임 시 동적으로 할당해 사용하는 공간
- new 연산자로 생성된 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장
- Method Area 영역에 올라온 클래스들만 객체로 생성할 수 있다.
Heap의 참조 주소는 스택이 가지고 있고, 해당 객체를 통해서만 Heap 영역에 있는 인스턴스를 핸들링할 수 있다. 기본 타입 변수는 Stack 영역에 직접 값을 가지고, 참조 타입 변수는 Heap 영역이나 Method 영역에 객체 주소를 가진다.

Person p = new Person("김펄슨", 20);
// p => Stack 영역
// Person("김펄슨", 20) => Heap 영역
📌 가비지 컬렉터에 대해
JVM은 가비지 컬렉터를 이용하여 Heap 영역에서 사용하지 않는 메모리를 회수한다. 즉, 더 이상 사용되지 않는 인스턴스를 찾아서 메모리에서 삭제한다.
자바는 가비지 컬렉터가 메모리 관리를 대신해주기 때문에, 한정된 메모리를 효율적으로 사용할 수 있게 하여 메모리 누수 문제에 대해서 관리하지 않고 편하게 개발에 집중할 수 있다.
for (int i = 0; i < 100; i++) {
MyClass my = new MyClass();
my.do();
}
위 코드에서 for문을 벗어나면, 생성된 MyClass들은 사용되지 않는다.
이런 객체들이 메모리를 계속 차지한다면, 우리가 사용가능한 메모리 자원은 줄어들게 된다. 이때 가바지 컬렉터(GC)가 이러한 객체들을 비움으로써 메모리를 효율적으로 사용할 수 있게 된다.
STW (Stop The World)
가비지 컬렉터가 자동으로 이러한 메모리 관리를 해주지만, 언제 수행하는지 정확하게 알 수 없어 제어하기 힘들고, 가바지 컬렉션이 동작하는 동안에는 다른 동작을 멈추기 때문에 오버헤드가 존재한다. 결국, GC가 자주 실행되면 그만큼 소프트웨어의 성능도 하락하게 된다.
이를 **STW(Stop The World)**라 한다.
가바지 컬렉션 대상
가비지 컬렉터는 특정 객체가 **도달가능성(Reachability)**이라는 개념을 도입한다.
객체의 레퍼런스가 존재하면 Reachable로 구분되고, 존재하지 않는다면 Unreachable로 구분하여 수거한다.
- Reachable : 객체가 참조되고 있음 ⇒ GC 대상 X
- Unreachable : 객체가 참조되고 있지 않음 ⇒ GC 대상

JVM 메모리에서는 객체들은 Heap 영역에서 생성되고 Method 영역이나 Stack 영역에서는 Heap Area에 생성된 객체의 주소만 참조하는 형식이다.
하지만 Heap 영역 객체의 참조가 삭제되면 해당 객체는 Unreachable 상태가 된다. 이러한 객체들을 가비지 컬렉터가 주기적으로 제거하게 된다.
가비지 컬렉션 방식
가비지 컬렉션은 Mark And Sweep 방식으로 Unreachable한 객체들을 제거한다.
- Mark : Root Space로부터 그래프 순회를 통해 연결된 객체를 찾아 어떤 객체를 참조하는지 마킹한다.
- Root Space
- 각 스레드의 Stack 로컬 변수
- Native Method Stack
- Method Area의 static 변수
- Root Space
- Sweep : Unreachable한 객체들을 Heap 영역에서 제거한다.
- Compact : Sweep 과정 후 분산된 객체들을 Heap 영역의 시작 주소로 모아 메모리가 할당된 부분과 할당되지 않은 부분으로 압축한다.
- 사용될 수도 있고 안될 수도 있다.
가비지 컬렉션 동작 과정
JVM의 Heap 영역은 동적으로 레퍼런스 데이터가 저장되는 공간으로 가바지 컬렉션의 대상이 되는 공간이다. Heap 영역은 두 가지 전제를 기반으로 설계되었다.
- 대부분의 객체는 금방 Unreachable 상태가 된다.
- 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
이에 따라 객체의 생존 기간에 따라 Heap 영역을 Young과 Old 두 영역으로 나누게 되었다.

- Young 영역
- 새롭게 생성된 객체가 할당되는 영역
- 대부분 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다 사라진다.
- Young 영역에 대한 GC를 Minor GC라 한다.
- Eden
- new를 통해 생성된 객체
- 정기적인 가비지 수집 후 살아남은 객체들은 Survivor 영역으로 보냄
- Survivor 0 / Survivor 1
- 최소 1번의 GC 이상 살아남은 객체가 존재
- Survivor 0 또는 Survivor 1 둘 중 하나는 꼭 비어 있어야 함
- Old 영역
- Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- Young 영역보다 크게 할당되며, 크기가 큰 만큼 가비지는 적게 발생
- Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않음
- Old 영역에 대한 가비지 컬렉션을 Major GC 또는 Full GC라 한다.
Heap 영역을 세부적으로 쪼갬으로써 객체의 생존 기간을 면밀히 제어하여 가비지 컬렉터를 보다 정확하게 불필요한 객체를 제거하도록 한다.
Minor GC
Young Generation 영역은 짧게 살아있는 메모리들이 존재한다. Young Generation은 Old Generation에 비해 상대적으로 작기 때문에 메모리 상의 객체를 찾아 제거하는데 시간이 적게 소요된다.
- 처음 생성된 객체 ⇒ Eden 영역
- Eden 영역이 꽉 차게 되면 Minor GC 실행
- Mark 동작을 통해 reachable 객체 탐색
- Eden 영역에서 살아남은 객체는 Survivor 영역으로 이동
- Eden 영역에서 사용되지 않은 객체의 메모리 해제
- 살아남은 객체들의 age 값이 1 증가
- 다시 Eden 영역이 꽉차게 되면 Minor GC 실행
- Mark 동작을 통해 마킹된 객체들은 비어있는 Survivor 영역으로 이동
- 살아남은 객체들의 age 값 1 증가
Major GC
Old Generation은 길게 살아있는 메모리들이 존재한다. GC 과정 중 제거되지 않은 경우, age 임계값이 차게 되어 옮겨진다.
Major GC는 Old 영역의 메모리가 부족해지면 발생하게 된다.
- 객체의 age가 임계값에 도달하면 Old Generation으로 이동된다.
- Old Generation 영역의 공간이 부족하면 Major GC 실행
- Old 영역에 있는 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제
Old Generation은 Young Generation에 비해 상대적으로 큰 공간을 가지고 있어, GC에 많은 시간이 걸리게 된다. Minor GC는 애플리케이션에 큰 영향을 주지 않지만, Major GC는 10배 이상의 시간을 사용한다.
가비지 컬렉션 알고리즘
위에서 보다시피 GC를 수행하는 과정에서 STW 문제가 발생할 수 있다. 이에 따라 자바는 발전하면서 애플리케이션 지연 현상이 두드러지게 되었고, 최적화를 위해 다양한 알고리즘이 개발되었다.
GC 알고리즘은 설정을 통해 Java에 적용할 수 있다.
Serial GC
- 서버 CPU 코어가 1개일 때 사용하기 위해 개발된 GC
- GC를 처리하는 스레드 = 1개
- STW 시간이 가장 김
- Minor GC에는 Mark-Sweep, Major GC에는 Mark-Sweep-Compact 사용
Parallel GC
- Java 8의 Default GC
- Sereal GC와 기본적인 알고리즘 같지만, Young 영역의 Minor GC를 멀티 스레드로 수행
Parallel Old GC
- Parallel GC 개선 버전
- Young 영역, Old 영역 둘 다 멀티 쓰레드로 GC 수행
- Mark-Summary-Compact 방식
CMS GC
- 애플리케이션 스레드와 GC 스레드가 동시에 실행 ⇒ STW 시간을 최대한 줄이기 위함
- GC 과정이 매우 복잡함
- GC 대상을 파악하는 과정에서 다른 GC 대비 CPU 사용량이 높음
- 메모리 파편화 문제
- Java 9부터 deprecated 되고, Java 14에서는 중지
G1 GC (Garbage First)
- CMS GC를 대체하기 위해 Java 7 에서 릴리즈
- Java 9+에서 Default GC로 지정
- Heap 영역을 Region이라는 개념을 도입하여 사용
- 역할을 고정이 아닌 동적으로 부여
- Young/Old 영역 X
- Garbage로 가득 찬 영역을 빠르게 회수하여 빈 공간 확보 ⇒ GC 빈도가 줄어듦
- 메모리가 많이 차있는 영역을 우선적으로 GC
- 영역(Region)을 나눠 탐색하고 영역별로 GC 수행
- 영역을 순차적으로 이동(Eden → Survivor) X
- 더 효율적이라고 생각하는 위치로 객체를 재할당
Shenandoah GC
- Java 12에 릴리즈
- 강력한 Concurrency와 가벼운 GC 로직으로 heap 사이즈에 영향을 받지 않고 일정한 pause 시간 소요
ZGC
- Java 15에 릴리즈
- 대량의 메모리를 low-latency로 처리하기 위해 디자인된 GC
- G1의 Region처럼, ZPage라는 영역을 사용
- 2mb 배수로 동적으로 운영
- Heap 크기가 증가하더라도, STW 시간이 절대 10ms를 넘지 않음
참고
https://doozi0316.tistory.com/entry/1주차-JVM은-무엇이며-자바-코드는-어떻게-실행하는-것인가
https://velog.io/@ddangle/Java-클래스-로더란
https://inpa.tistory.com/entry/JAVA-☕-JVM-내부-구조-메모리-영역-심화편
https://inpa.tistory.com/entry/JAVA-☕-가비지-컬렉션GC-동작-원리-알고리즘-💯-총정리
'자바' 카테고리의 다른 글
Java 21 Virtual Thread 간단히 알아보기 (0) | 2025.04.06 |
---|---|
자바 상속에 대해서 알아보기 (0) | 2025.01.10 |