| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 오라클 캐릭터셋 확인
- 오라클 캐릭터셋 변경
- Orace 18c
- Oracle 18c HR
- Oracle 윈도우 설치
- oracle
- 무료 오라클 데이터베이스
- Oracle 사용자명 입력
- Oracle 18c 설치
- ORA-12899
- 윈도우 Oracle
- 서평단
- 무료 오라클 설치
- oracle 18c
- 오라클 캐릭터셋 조회
- Oracle 18c HR schema
- Oracle 초기 사용자
- 비전공자를 위한 데이터베이스 입문
- Oracle 테이블 대소문자
- ora-01722
- Oracle 사용자명
- Oracle 테이블 띄어쓰기
- ORA-00922
- Oracle Express Edition
- Today
- Total
The Nirsa Way
[JAVA] 핫스팟의 객체 표현과 가비지 컬렉션 루트 본문
핫스팟의 객체 표현과 가비지 컬렉션 루트
가비지 컬렉션은 힙에 생성된 객체 중 더 이상 사용할 수 없는 객체가 차지한 메모리를 회수합니다.
그렇다면 JVM은 힙에 있는 객체를 어떤 형태로 관리하고, 어떤 객체가 살아 있는지 어디서부터 확인할까요?
이 질문에 답하려면 다음 두 가지를 구분해서 이해해야 합니다.
객체 표현
→ 힙에 생성된 객체가 어떤 구조로 저장되는가
객체 탐색
→ 가비지 컬렉터가 어디서부터 객체를 찾아가는가
핫스팟 JVM에서는 힙의 객체를 내부적으로 OOP라는 구조를 통해 다룹니다. 객체의 앞부분에는 객체 상태와 클래스 정보를 담는 객체 헤더가 위치합니다.
가비지 컬렉터는 객체 구조만 보고 생존 여부를 판단하지 않습니다. GC Root라고 부르는 시작점에서 객체 참조를 따라가며 도달 가능한 객체를 찾습니다.
- 핫스팟은 자바 애플리케이션을 실행하는 대표적인 JVM 구현체입니다.
- OOP는 핫스팟 내부에서 자바 객체를 가리키고 다루기 위한 표현입니다.
- 객체 헤더는 Mark Word와 Klass Word로 구성됩니다.
- GC Root는 가비지 컬렉터가 살아 있는 객체를 찾기 시작하는 출발점입니다.
핫스팟이란
핫스팟(HotSpot)은 자바 바이트코드를 실행하는 대표적인 JVM 구현체입니다.
자바 소스 코드는 먼저 컴파일러에 의해 바이트코드로 변환됩니다. 이후 JVM이 바이트코드를 실행합니다.

JVM은 자바 프로그램 실행에 필요한 여러 기능을 담당합니다.
| 기능 | 역할 |
| 클래스 로딩 | 필요한 클래스 파일을 JVM 메모리에 적재합니다. |
| 바이트코드 실행 | 컴파일된 자바 바이트코드를 실행합니다. |
| JIT 컴파일 | 자주 실행되는 코드를 기계어로 컴파일합니다. |
| 메모리 관리 | 힙, 스택 등의 런타임 메모리를 관리합니다. |
| 가비지 컬렉션 | 사용하지 않는 객체의 메모리를 회수합니다. |
핫스팟이라는 이름은 프로그램에서 자주 실행되는 코드 영역인 핫스팟을 찾아 최적화한다는 의미에서 붙었습니다.
처음에는 바이트코드를 실행하다가 특정 코드가 반복적으로 실행되는 것이 확인되면, JIT 컴파일러가 해당 코드를 운영체제와 CPU가 직접 실행할 수 있는 기계어로 변환합니다.

핫스팟은 자바 언어 자체가 아니라 JVM 명세를 구현한 실행 환경입니다.
따라서 이번 글에서 다루는 OOP, Mark Word, Klass Word와 같은 표현은 자바 언어 문법이라기보다 핫스팟 JVM 내부 구현과 관련된 개념입니다.
핫스팟은 객체를 어떻게 표현할까
자바 코드에서는 참조 변수를 사용해 객체에 접근합니다.
User user = new User();
이 코드에서 user는 객체 자체가 아닙니다. 힙에 생성된 User 객체를 가리키는 참조 변수입니다.
핫스팟 내부에서는 자바 객체를 가리키고 다루기 위한 표현으로 OOP를 사용합니다.
OOP는 일반적으로 Ordinary Object Pointer를 의미합니다. 핫스팟 내부 코드에서는 객체를 가리키는 포인터 형태의 추상화로 사용됩니다.
자바 코드 관점
참조 변수 → 객체
핫스팟 내부 관점
OOP → 힙에 존재하는 객체 구조
다만 자바 개발자가 작성하는 참조 변수가 C나 C++의 포인터와 완전히 같다는 뜻은 아닙니다.
자바에서는 개발자가 객체의 실제 메모리 주소를 직접 계산하거나 변경할 수 없습니다. 객체 이동과 참조 갱신은 JVM이 관리합니다.
힙의 객체는 어떻게 구성될까
힙에 생성되는 일반적인 인스턴스 객체는 크게 다음 영역으로 구성됩니다.
- 객체 헤더
- 인스턴스 필드
- 정렬 패딩
배열 객체에는 여기에 배열 길이 정보가 추가됩니다.

객체의 실제 크기와 배치는 다음 조건에 따라 달라질 수 있습니다.
- 32비트 또는 64비트 JVM
- 객체 포인터 압축 사용 여부
- 클래스 포인터 압축 사용 여부
- 필드의 타입과 배치
- 객체 정렬 단위
- JVM 버전과 구현 방식
따라서 객체 구조를 이해할 때는 고정된 바이트 크기보다 각 영역의 역할을 먼저 이해하는 것이 좋습니다.
객체 헤더에는 어떤 정보가 있을까
일반적인 핫스팟 객체 헤더에는 다음 두 영역이 포함됩니다.
- Mark Word
- Klass Word
배열 객체는 객체 헤더에 배열 길이 정보가 추가됩니다.
| 객체 구성 | 주요 역할 |
| Mark Word | 객체의 실행 상태와 GC 관련 정보를 저장합니다. |
| Klass Word | 객체가 어떤 클래스의 인스턴스인지 찾는 데 사용됩니다. |
| 인스턴스 필드 | 개발자가 클래스에 선언한 필드값을 저장합니다. |
| 배열 길이 | 배열이 가진 요소의 개수를 저장합니다. |
| 배열 요소 | 배열에 저장된 실제 데이터를 담습니다. |
| 정렬 패딩 | 객체 크기를 JVM의 정렬 기준에 맞춥니다. |
Mark Word란
Mark Word는 객체의 실행 상태와 관련된 여러 정보를 저장하는 영역입니다.
JVM의 상태와 객체의 현재 상황에 따라 다음과 같은 정보가 저장될 수 있습니다.
- 객체의 아이덴티티 해시 코드
- 객체의 잠금 상태
- GC에서 사용하는 객체 나이
- 마킹 상태와 관련된 정보
- 모니터 또는 잠금 구조와 연결되는 정보

여기서 주의할 점이 있습니다.
Mark Word를 단순히 객체 메타데이터를 가리키는 포인터라고 설명하는 것은 정확하지 않습니다.
Mark Word는 특정 객체의 상태를 표현하기 위한 비트 영역입니다. 객체가 잠겨 있는지, 해시 코드가 계산되었는지, GC 과정에서 어떤 상태인지 등에 따라 저장되는 내용의 해석이 달라질 수 있습니다.
※ Mark Word는 객체의 클래스 종류를 저장하는 영역이 아닙니다.
객체 장금 상태, 해시 코드, GC 상태와 같은 객체 자체의 상태를 담는 영역입니다.
또한 여기서 말하는 Mark Word와 가비지 컬렉션의 Mark 단계는 이름은 비슷하지만 같은 개념은 아닙니다.
| 구분 | 의미 |
| Mark Word | 객체 헤더에 포함된 데이터 영역입니다. |
| GC의 Mark 단계 | GC Root에서 도달 가능한 객체를 표시하는 과정입니다. |
Klass Word란
Klass Word는 해당 객체가 어떤 클래스의 인스턴스인지 확인할 수 있도록 클래스 메타데이터와 연결하는 영역입니다.
예를 들어 힙에 User 객체가 생성되었다면, 객체의 Klass Word를 통해 해당 객체가 User 클래스의 인스턴스라는 정보를 확인할 수 있습니다.

Klass 메타데이터에는 클래스를 실행하고 관리하기 위한 정보가 포함될 수 있습니다.
- 클래스 이름과 타입 정보
- 필드 정보
- 메서드 정보
- 상위 클래스 정보
- 인터페이스 정보
- 객체 크기와 배치에 관한 정보
Klass 메타데이터는 일반적인 자바 인스턴스 객체와 동일하게 힙에 저장되는 것이 아닙니다.
현대적인 핫스팟 JVM에서는 클래스 메타데이터가 주로 메타스페이스(Metaspace)라는 네이티브 메모리 영역에 관리됩니다.

다만 Class<User>와 같이 자바 코드에서 다루는 java.lang.Class 객체는 힙에 존재할 수 있습니다.
따라서 다음 두 개념을 구분해야 합니다.
| 구분 | 저장 위치와 역할 |
| Klass 메타데이터 | 핫스팟 내부의 클래스 메타데이터이며 주로 메타스페이스에 존재합니다. |
| java.lang.Class 객체 | 자바 코드에서 클래스 정보를 다루기 위한 객체이며 힙에 존재합니다. |
인스턴스 객체와 배열 객체의 차이
일반 인스턴스 객체와 배열 객체는 모두 객체 헤더를 가집니다.
하지만 배열은 몇 개의 요소를 가지고 있는지 JVM이 알아야 하므로 배열 길이 정보가 추가됩니다.
인스턴스 객체

인스턴스 객체의 데이터 영역에는 클래스에 선언한 필드값이 저장됩니다.
class User {
int age;
String name;
}
User 객체에는 개념적으로 age 값과 name 참조가 인스턴스 필드 영역에 저장됩니다.
배열 객체

배열 객체는 다음 정보를 가집니다.
배열의 객체 상태
배열의 타입
배열의 길이
배열의 각 요소
예를 들어 다음 배열이 있다고 가정하겠습니다.
int[] numbers = new int[3];
개념적인 객체 구조는 다음과 같습니다.
Mark Word
Klass Word
배열 길이: 3
요소: 0, 0, 0
정렬 패딩
인스턴스 객체 vs 배열 객체
| 구분 | 인스턴스 객체 | 배열 객체 |
| Mark Word | 포함합니다. | 포함합니다. |
| Klass Word | 포함합니다. | 포함합니다. |
| 데이터 | 인스턴스 필드 | 배열 요소 |
| 길이 정보 | 별도로 필요하지 않습니다. | 배열 길이를 저장합니다. |
| 크기 | 필드 구성에 따라 결정됩니다. | 요소 타입과 개수에 따라 결정됩니다. |
객체 구조와 GC Root는 다른 개념입니다
OOP와 객체 헤더는 힙의 객체가 어떤 구조로 표현되는지 설명합니다.
반면 GC Root는 가비지 컬렉터가 어떤 위치에서 객체 탐색을 시작하는지 설명합니다.

| 구분 | 설명하는 대상 |
| OOP | 핫스팟 내부에서 객체를 가리키는 표현 |
| 객체 헤더 | 개별 객체의 상태와 클래스 정보 |
| GC Root | 객체 생존 여부를 판단하기 위한 탐색 시작점 |
| 참조 그래프 | 객체들이 서로 연결된 관계 |
객체 헤더가 있다고 해서 해당 객체가 반드시 살아 있는 것은 아닙니다.
객체가 활성 객체인지 판단하려면 GC Root에서 해당 객체까지 도달할 수 있는지 확인해야 합니다.
가비지 컬렉션 루트란
가비지 컬렉션 루트(GC Root)는 가비지 컬렉터가 살아 있는 객체를 찾기 위해 참조 탐색을 시작하는 출발점입니다.
GC는 힙의 객체를 하나씩 보면서 사용할 것인지 질문하는 방식으로 동작하지 않습니다.
먼저 JVM이 현재 실행을 위해 반드시 유지해야 하는 참조들을 GC Root로 정합니다. 그다음 GC Root에서 객체 참조를 따라가며 도달 가능한 객체를 찾습니다.

위 구조에서는 GC Root에서 다음 객체에 도달할 수 있습니다.
- 객체 A
- 객체 B
- 객체 C
- 객체 D
따라서 네 객체는 활성 객체로 판단됩니다.
객체 X와 객체 Y는 서로를 참조하지만 GC Root에서 접근할 수 있는 경로가 없습니다.
객체 X ⇄ 객체 Y
GC Root에서 접근할 수 없음
→ Mark되지 않음
→ 가비지 컬렉션 대상
GC Root가 필요한 이유
힙에 객체가 존재한다는 사실만으로는 객체가 실제로 사용 중인지 판단할 수 없습니다.
예를 들어 다음 두 객체는 모두 힙에 존재합니다.
flowchart LR
R[GC Root] --> A[객체 A]
X[객체 X]
하지만 두 객체의 상태는 다릅니다.
| 구분 | 객체힙에 존재 | GC Root에서 도달 가능 | 판단 |
| 객체 A | 예 | 예 | 활성 객체 |
| 객체 X | 예 | 아니요 | 가비지 객체 |
객체 X가 힙에 남아 있다고 해서 살아 있는 객체는 아닙니다.
가비지 컬렉션에서 객체의 생존 여부를 결정하는 핵심 기준은 도달 가능성(Reachability)입니다.
GC Root에서 도달 가능
→ 프로그램이 다시 사용할 가능성이 있음
→ 활성 객체
GC Root에서 도달 불가능
→ 프로그램에서 다시 접근할 방법이 없음
→ 가비지 객체
이 탐색 결과는 Mark and Sweep의 Mark 단계에서 어떤 객체를 표시할지 결정하는 기준이 됩니다.
주요 GC Root에는 무엇이 있을까
GC Root는 특정 객체 한 개를 의미하는 것이 아닙니다.
JVM이 현재 프로그램 실행을 위해 유효하다고 판단하는 여러 참조의 집합입니다.
대표적인 GC Root는 다음과 같습니다.
| GC Root 유형 | 설명 |
| 실행 중인 스레드의 스택 참조 | 메서드의 지역 변수와 매개변수 등이 참조하는 객체입니다. |
| CPU 레지스터의 참조 | 현재 실행 중인 코드가 레지스터에 보관하고 있는 객체 참조입니다. |
| 정적 필드가 참조하는 객체 | 로딩된 클래스의 static 필드가 참조하는 객체입니다. |
| JNI 참조 | 네이티브 코드에서 JNI를 통해 유지 중인 자바 객체입니다. |
| 실행 중인 스레드 객체 | JVM이 실행 중인 스레드를 관리하며 유지하는 참조입니다. |
| JVM 내부 참조 | 클래스 로더, 시스템 클래스와 같이 JVM이 내부적으로 유지하는 객체입니다. |
| 코드 캐시의 참조 | JIT 컴파일된 코드가 포함하고 있는 객체 또는 메타데이터 참조입니다. |
스택 프레임의 지역 변수
다음 코드를 살펴보겠습니다.
public void process() {
User user = new User();
Order order = new Order();
user.setOrder(order);
}
process() 메서드가 실행 중이라면, 해당 메서드의 스택 프레임에 user와 order 참조가 존재할 수 있습니다.

실행 중인 스택 프레임의 참조는 GC 탐색의 출발점이 될 수 있습니다.
따라서 User 객체와 Order 객체는 GC Root에서 도달 가능한 활성 객체입니다.
정적 필드도 GC Root가 될 수 있을까
정적 필드가 참조하는 객체도 살아 있는 객체 탐색의 출발점이 될 수 있습니다.
public class UserCache {
private static final Map<Long, User> CACHE = new HashMap<>();
}
CACHE가 로딩된 클래스의 정적 필드에서 계속 참조되고 있다면, 그 안에 저장된 객체도 참조 관계를 통해 도달할 수 있습니다.

이러한 객체는 일반적으로 GC Root에서 도달 가능하므로 가비지 컬렉션 대상이 되지 않습니다.
이 때문에 정적 컬렉션에 객체를 계속 추가하고 제거하지 않으면 메모리 누수가 발생할 수 있습니다.
static 컬렉션
↓
객체를 계속 참조
↓
GC Root에서 계속 도달 가능
↓
사용하지 않아도 수집되지 않음
↓
힙 사용량 지속 증가
자바에서 말하는 메모리 누수는 메모리를 직접 해제하지 않아 발생하는 문제만을 의미하지 않습니다.
프로그램에서는 더 이상 필요하지 않지만, 참조가 남아 있어 GC가 회수하지 못하는 객체도 메모리 누수의 원인이 될 수 있습니다.
배열은 GC Root인가
배열은 그 자체만으로 일반적인 GC Root 유형이라고 보기는 어렵습니다.
배열도 힙에 생성되는 자바 객체이므로, 다른 객체와 마찬가지로 GC Root에서 도달 가능한지를 판단해야 합니다.

GC Root가 배열을 참조하고 있다면 배열은 활성 객체입니다. 배열이 각 요소를 통해 다른 객체를 참조하고 있다면, 그 객체들도 배열을 거쳐 도달할 수 있으므로 활성 객체가 됩니다.
반대로 배열을 가리키는 경로가 GC Root에서 모두 끊어지면 배열과 배열만을 통해 접근하던 객체도 가비지 컬렉션 대상이 될 수 있습니다.
GC Root에서 객체를 탐색하는 과정
다음과 같은 객체 관계가 있다고 가정하겠습니다.

가비지 컬렉터는 GC Root에서 탐색을 시작합니다.
스택의 지역 변수
→ 객체 A
→ 객체 B
→ 객체 C
정적 필드
→ 객체 D
→ 객체 E
따라서 객체 A, B, C, D, E는 활성 객체로 Mark됩니다.
객체 X와 객체 Y는 서로를 참조하지만 GC Root에서 이어지는 경로가 없습니다. 따라서 Mark되지 않으며 메모리 회수 대상이 될 수 있습니다.

참조 개수보다 도달 가능성이 중요한 이유
객체가 여러 번 참조된다는 사실만으로 살아 있는 객체라고 판단할 수는 없습니다.
다음과 같이 두 객체가 서로를 참조할 수 있습니다.

객체 A는 객체 B의 참조를 받고 있고, 객체 B도 객체 A의 참조를 받고 있습니다.
참조 개수만 보면 두 객체 모두 참조되고 있습니다.
하지만 GC Root에서 두 객체까지 도달할 수 있는 경로가 없다면 실제 프로그램에서는 두 객체를 사용할 수 없습니다.
객체 A가 객체 B를 참조
객체 B가 객체 A를 참조
그러나 GC Root에서 접근 불가능
→ 두 객체 모두 가비지 객체
따라서 핫스팟의 가비지 컬렉션을 이해할 때는 참조 개수보다 GC Root에서 출발한 참조 경로를 중심으로 봐야 합니다.
객체 표현부터 생존 판단까지의 전체 흐름
지금까지 살펴본 내용을 하나의 흐름으로 정리하면 다음과 같습니다.

객체 헤더는 객체의 구조와 상태를 설명합니다.
GC Root는 해당 객체가 현재 프로그램에서 필요한 객체인지 판단하기 위한 탐색의 출발점입니다.
객체 헤더
→ 이 객체가 어떤 객체이며 어떤 상태인가
GC Root
→ 이 객체에 현재 프로그램이 도달할 수 있는가
핵심 정리
- 핫스팟은 자바 바이트코드를 실행하는 대표적인 JVM 구현체입니다.
- 핫스팟은 자주 실행되는 코드를 찾아 JIT 컴파일을 통해 최적화합니다.
- OOP는 핫스팟 내부에서 힙의 자바 객체를 가리키고 다루기 위한 표현입니다.
- 일반적인 객체는 객체 헤더, 객체 데이터, 정렬 패딩으로 구성됩니다.
- 객체 헤더에는 Mark Word와 Klass Word가 포함됩니다.
- Mark Word에는 객체의 잠금 상태, 해시 코드, GC 관련 정보 등이 저장될 수 있습니다.
- Klass Word는 객체가 어떤 클래스의 인스턴스인지 확인할 수 있도록 Klass 메타데이터와 연결합니다.
- 배열 객체에는 일반 객체 헤더와 함께 배열 길이 정보가 추가됩니다.
- GC Root는 가비지 컬렉터가 활성 객체를 찾기 시작하는 출발점입니다.
- 실행 중인 스레드의 스택, 레지스터, 정적 필드, JNI 참조, JVM 내부 참조 등이 GC Root가 될 수 있습니다.
- 객체의 생존 여부는 참조 개수가 아니라 GC Root에서 도달할 수 있는지에 따라 결정됩니다.
- 배열은 그 자체가 GC Root라기보다 GC Root에서 도달 가능한 힙 객체로 이해해야 합니다.
마무리
핫스팟은 힙에 생성된 자바 객체를 OOP라는 내부 표현을 통해 관리합니다.
각 객체의 앞부분에는 Mark Word와 Klass Word로 구성된 객체 헤더가 있습니다. Mark Word는 객체의 잠금 상태와 GC 관련 상태 등을 담고, Klass Word는 객체의 클래스 메타데이터를 찾는 데 사용됩니다.
하지만 객체 구조만으로는 객체가 현재 사용 중인지 알 수 없습니다.
가비지 컬렉터는 스택의 지역 변수, 정적 필드, JNI 참조와 같은 GC Root에서 출발해 객체 참조를 따라갑니다. 이 과정에서 도달할 수 있는 객체는 활성 객체로 Mark하고, 도달할 수 없는 객체는 메모리 회수 대상으로 판단합니다.

이 구조를 이해하면 가비지 컬렉션이 단순히 힙의 객체를 임의로 삭제하는 것이 아니라, JVM이 관리하는 명확한 출발점과 참조 관계를 기준으로 객체의 생존 여부를 판단한다는 점을 이해할 수 있습니다.
