GC(Garbage Collector)가 관리하는 메모리 영역은 힙(Heap)과 메서드 영역(Method Area)입니다. 각 영역은 서로 다른 방식으로 회수가 이루어지는데, 이번 글에서는 그 차이와 핵심 개념들을 정리해보겠습니다.
힙 영역: 도달 가능성 분석
왜 참조 카운팅이 아닌가?
참조 카운팅(Reference Counting) 방식은 구현이 단순하지만, 순환 참조 문제가 있습니다.
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;
// 참조 카운팅: a, b 모두 참조 카운트 1 → 수거 불가
// 도달 가능성 분석: GC Roots에서 도달 불가 → 수거 가능
이런 이유로 JVM은 도달 가능성 분석(Reachability Analysis) 알고리즘을 사용합니다.
도달 가능성 분석이란?
핵심 아이디어는 간단합니다: GC Roots에서 시작해서 참조 체인을 따라갈 수 없는 객체는 수거 대상이 됩니다.
GC Roots가 될 수 있는 객체들
- 스택 프레임의 지역 변수와 파라미터: 현재 실행 중인 메서드들이 참조하는 객체
- 정적 변수: 클래스의 static 필드가 참조하는 객체
- JNI 참조: 네이티브 코드에서 생성된 전역/로컬 참조
- 활성 스레드: 살아있는 Thread 객체 자체
- 동기화 모니터: synchronized 블록에서 사용 중인 객체
- JVM 내부 참조: 클래스 로더, 기본 예외 객체(NPE, OOM 등), 시스템 클래스
finalize()와 2단계 회수 (레거시)
도달 불가능 판정을 받아도, finalize()를 오버라이드한 객체는 한 번 더 기회를 받습니다. F-Queue에 등록되어 Finalizer 스레드가 실행한 후, 다음 GC에서 다시 검사합니다.
다만 finalize()는 Java 9에서 deprecated, Java 18에서 forRemoval 표시되었습니다. 새 코드에서는 Cleaner API를 사용하세요.
Cleaner는 정리 로직을 별도 객체에 분리하기 때문에, 원본 객체에 대한 this 참조가 없습니다. 따라서 부활 자체가 구조적으로 불가능하고, 2단계 회수 과정 없이 바로 정리됩니다.
| finalize() | Cleaner | |
|---|---|---|
| 부활 가능 | O | X |
| 회수 사이클 | 최소 2번 | 1번 |
메서드 영역: 타입 언로딩
힙과 다른 회수 방식
메서드 영역은 도달 가능성 분석이 아닌 타입 언로딩 조건 검사로 회수됩니다.
| 영역 | 회수 단위 | 판단 방식 |
|---|---|---|
| 힙 | 개별 객체 | GC Roots → 참조 체인 추적 |
| 메서드 영역 | 클래스 전체 | 세 가지 조건 모두 만족 여부 |
타입 언로딩의 세 가지 조건
클래스가 언로딩되려면 세 가지 조건이 모두 만족되어야 합니다:
- 해당 클래스의 모든 인스턴스가 힙에서 회수됨
- 해당 클래스를 로딩한 ClassLoader가 회수됨
- 해당 클래스의 Class 객체가 어디서도 참조되지 않음
왜 회수 대상이 거의 없는가?
일반적인 애플리케이션에서는 대부분의 클래스가 Bootstrap/System ClassLoader로 로딩됩니다. 이 로더들은 JVM 종료 전까지 살아있기 때문에, 언로딩 조건 자체가 충족되지 않습니다.
GC가 메서드 영역을 "안 하는" 게 아니라, 조건을 검사해봐도 언로딩할 클래스가 없는 것입니다.
바이트코드 프레임워크 환경에서는 다르다
CGLib, ASM, ByteBuddy, Javassist 같은 프레임워크는 런타임에 새로운 클래스를 생성합니다. Spring AOP, Hibernate 지연 로딩 등이 이 방식을 사용하죠.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new MethodInterceptor() { ... });
UserService proxy = (UserService) enhancer.create();
// → UserService$$EnhancerByCGLIB$$xxxx 같은 새 클래스가 Metaspace에 로딩됨
이런 환경에서는:
- 동적 클래스가 자체 ClassLoader를 통해 로딩됨
- 언로딩 조건이 실제로 충족될 수 있음
- 제대로 정리 안 되면 Metaspace 누수로 이어짐
안전장치 및 모니터링
# Metaspace 무한 증가 방지
-XX:MaxMetaspaceSize=256m
-XX:MetaspaceSize=128m
# 클래스 언로딩 활성화 확인
-XX:+ClassUnloading
-XX:+ClassUnloadingWithConcurrentMark # G1
다만 이 설정만으로 누수가 해결되는 건 아닙니다. 클래스 로더 누수나 캐시 미정리 등 근본 원인을 파악하는 게 중요합니다.
정리
| 구분 | 힙 | 메서드 영역 |
|---|---|---|
| 회수 단위 | 개별 객체 | 클래스 전체 |
| 알고리즘 | 도달 가능성 분석 | 타입 언로딩 조건 검사 |
| 회수 빈도 | 자주 (Minor/Major GC) | 드물게 |
| 주의 상황 | 일반적인 객체 관리 | 바이트코드 프레임워크 사용 시 |
GC의 핵심은 힙의 도달 가능성 분석이지만, 동적 클래스 생성이 많은 환경에서는 메서드 영역 관리도 신경 써야 합니다.
'CS > JVM' 카테고리의 다른 글
| [JVM 밑바닥까지 파헤치기] JVM 가비지 컬렉터의 역사 (1) - 초기 컬렉터: Serial & Serial Old (0) | 2026.01.25 |
|---|---|
| [JVM 밑바닥까지 파헤치기] HotSpot GC의 상세 구현 기법 (0) | 2026.01.25 |
| [JVM 밑바닥까지 파헤치기] JVM GC의 이론적 기반 쌓기 - 세대 단위 컬렉션, GC 유형과, 기본 알고리즘 (0) | 2026.01.25 |
| [JVM 밑바닥까지 파헤치기] JVM의 진화: Classic VM에서 HotSpot VM까지 (0) | 2026.01.21 |
| [JVM 밑바닥까지 파헤치기] JIT vs AOT 컴파일러, 그리고 JVM과 GraalVM에 대하여 (1) | 2026.01.17 |