GarbageCollector gc에 대해 알아보자.
GarbageCollector는 프로그램의 효율적인 메모리 관리(메모리 누수 방지)를 위한 시스템, 혹은 프로그램의 구성요소이다.
메모리에서 더이상 사용하지 않는 객체를 제거하여 메모리를 확보하는 역할을 함.
메모리 공간은 한정적이기 때문에 사용이 끝난 객체는 제거하여 메모리를 확보해야 되는데,
C에서는 GarbageCollector 가 존재하지 않아 malloc() 등으로 할당한 공간을 사용이 끝났을 때 직접 free()와 같은 함수를 통해 메모리를 확보해 주어야 된다. /혹은 외부 라이브러리 사용해야 됨.
Java에서는 new 키워드로 객체를 생성해 주며, 메모리 해제는 jvm의 GarbageCollector가 관리해 준다.
더 이상 참조되지 않는 객체는 gc가 자동으로 객체를 정리해 메모리를 관리해 준다.
jvm내부 gc의 단점
- GC가 실행될 때 애플리케이션 성능이 순간적으로 저하될 수 있음
- 메모리를 즉시 반환하지 않기 때문에 예상보다 많은 메모리를 사용할 가능성 있음
- GC가 언제 실행될지 개발자가 정확하게 제어할 수 없음
gc의 동작 방식
일반적으로 Mark & Sweep, Reference Counting, Generational GC 등의 알고리즘을 사용하여 동작한다.
Mark&Sweep
Mark 단계:
- 루트 객체(root objects, 즉 GC Root)에서 시작하여 도달할 수 있는 객체들을 **"Mark (표시)"**함
- 도달할 수 없는(사용되지 않는) 객체들은 표시되지 않음
Sweep 단계:
- Mark 되지 않은 객체(도달 불가능한 객체)들을 메모리에서 해제(free)
- 이후, 필요하면 **메모리 정리(Compaction)**를 수행하여 메모리 단편화(fragmentation)를 줄임
- 장점
1. 순환참조 문제 해결가능
2. 간단한 알고리즘으로 안정적인 동작
- 단점
1. GC실행 중 프로그램이 멈춤 (Stop-the-World) GC가 진행되는 동안 애플리케이션의 모든 쓰레드가 중단됨.
2. GC실행시간이 객체 수에 비례해 길어질 수 있음.
* STW 발생 이유 ->
* Mark & Sweep에서 STW가 발생하는 이유 *
🔹 1. Mark 단계에서 모든 객체를 추적해야 함
- GC는 Mark 단계에서 "어떤 객체가 살아있는지"를 확인해야 함
- 하지만, 애플리케이션이 동시에 실행되면 객체의 참조 관계가 계속 변할 수 있음
- 이를 방지하기 위해 모든 쓰레드를 중단(STW)하고 GC가 안전하게 객체를 추적하도록 만듦
🔹 2. Sweep 단계에서 객체 해제 시 충돌 방지 필요
- Mark 단계가 끝나면 Sweep 단계에서 필요 없는 객체들을 제거
- 만약 애플리케이션이 실행 중이라면, 객체를 사용하는 도중에 삭제될 위험이 있음
-> 이러한 충돌을 방지하기 위해 STW가 필요함
🔹 3. Heap 메모리 정리(Compaction) 시 데이터 이동
- 객체를 해제한 후 메모리 단편화(fragmentation)를 방지하려면 **Compaction(압축)**이 필요
- Compaction 시 객체를 다른 메모리 위치로 이동시키는 경우가 발생
- 예: A -> B -> C 객체가 있을 때, B가 해제되면 C를 B의 위치로 이동시켜야 할 수도 있음
- 이동 중에 프로그램이 실행되면 객체 참조 오류가 발생할 수 있음
- 그래서 STW로 모든 작업을 멈춘 후 안전하게 객체를 이동해야 함
GC가 안전하게 객체를 탐색(Mark)하고 정리(Sweep)하려면 모든 쓰레드를 멈춰야 하기 때문.
하지만 최신 GC (G1, ZGC 등)는 STW 시간을 최소화하는 방향으로 발전 중.
// JVM의 GC가 Mark & Sweep 방식으로 동작
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = null; // obj1이 가리키는 객체는 GC 대상이 됨
// obj1이 null이 되어 더이상 사용되지 않으면, GC가 이를 탐색해 제거.
Reference Counting
1. 객체가 참조될 때마다 참조 카운터를 증가
2. 참조가 사라질 때 마다 참조 카운터 감소
3. 참조 카운터가 0이 되면 해당 객체를 삭제
- 장점
1. 간단한 구현 (바로바로 객체 해제 가능)
2. 실시간으로 메모리 해제가 가능하여 GC Pause(중단)가 적음
- 단점
1. 순환참조 문제 발생 가능.
예를 들어 A -> B , B -> A를 서로 참조한다면, GC가 수거하지 못함.
2. 대부분의 GC는 이 방식을 단독으로 사용하지 않음
import java.util.HashMap;
import java.util.Map;
class RefCountedObject {
private static Map<RefCountedObject, Integer> referenceCountMap = new HashMap<>();
public RefCountedObject() {
referenceCountMap.put(this, 1); // 처음 생성 시 참조 카운트를 1로 설정
}
public void addReference() {
referenceCountMap.put(this, referenceCountMap.get(this) + 1);
}
public void removeReference() {
int count = referenceCountMap.get(this) - 1;
if (count == 0) {
referenceCountMap.remove(this); // 참조 카운트가 0이면 객체 제거
System.out.println(this + " is garbage collected.");
} else {
referenceCountMap.put(this, count);
}
}
}
public class ReferenceCountingExample {
public static void main(String[] args) {
RefCountedObject obj1 = new RefCountedObject();
RefCountedObject obj2 = new RefCountedObject();
obj1.addReference(); // obj1의 참조 카운트 증가
obj2.addReference(); // obj2의 참조 카운트 증가
obj1.removeReference(); // 참조 해제 (1 → 0) → obj1 제거
obj1.removeReference(); // 추가 해제 시 이미 제거되어 아무 일도 없음
obj2.removeReference(); // 참조 해제 (1 → 0) → obj2 제거
obj2.removeReference(); // 추가 해제 시 이미 제거되어 아무 일도 없음
}
}
순환참조 예시
class CircularRef {
CircularRef reference;
}
public class CircularReferenceExample {
public static void main(String[] args) {
CircularRef obj1 = new CircularRef();
CircularRef obj2 = new CircularRef();
obj1.reference = obj2; // obj1 → obj2
obj2.reference = obj1; // obj2 → obj1 (순환 참조)
obj1 = null;
obj2 = null;
System.gc(); // GC 실행 요청
System.out.println("Garbage Collection requested.");
}
}
Copying GC
1. 힙 메모리를 **두 개의 영역(Semi-space)**으로 나누고, 한쪽에서만 할당
2. GC 실행 시 사용 중인 객체를 다른 영역으로 복사하고, 기존 영역을 한 번에 해제
- 장점
1. 메모리 단편화(Fragmentation) 문제 해결
2. 빠른 할당과 해제가 가능
- 단점
1. 전체 힙 메모리의 절반이 비워져 있어야 하므로 메모리 낭비가 있음
2. 객체 이동 비용이 발생함
사용예시 - JVM의 Young Generation(새로운 객체) 관리에 사용
Generational GC
1. 객체의 생존 기간에 따라 여러 개의 영역으로 분리하여 관리
2. 객체의 특성을 반영해 GC 성능 최적화.
- 메모리 구조 -
- Young Generation (새로운 객체)
- 대부분의 객체는 생성 후 곧바로 사라짐 → 빠르게 GC 수행
- Eden → Survivor → Old로 이동
- Old Generation (오래된 객체)
- 생존 시간이 긴 객체 → 가끔씩 GC 수행
- Permanent Generation (클래스 메타데이터 저장, 이후 Metaspace로 대체됨)
- 장점
1. young generation에서 빠르게 객체를 제거하므로 GC성능 최적화
2. old generation에서는 Mark&Sweep을 수행해 메모리 정리.
- 사용예시 - Java의 HotSpot JVM에서 G1 GC, Parallel GC 등이 Generational GC를 사용
GC동작 과정
1. 객체 생성
- new 키워드로 객체를 생성하면 Young Generation의 Eden 영역에 할당
2. Minor GC (Young Generation에서 수행)
- Eden 영역이 가득 차면 Survivor로 이동
- 여러 번 살아남은 객체는 Old Generation으로 이동
3. Major GC (Old Generation에서 수행)
- Mark & Sweep을 실행하여 사용되지 않는 객체 정리
4. Full GC (전체 힙에서 수행)
- 모든 영역에서 GC를 수행하며, 가장 비용이 큼
+ 또한 GC튜닝을 통해 성능 최적화를 할 수 있음.
GC 최적화 방법 ->
- 객체 생명주기 관리: 불필요한 객체를 빨리 null 처리
- 메모리 할당 줄이기: StringBuilder 사용하여 객체 생성 최소화
- GC 로그 분석: GC 횟수, 시간 확인하여 최적화
- JVM GC 튜닝 가이드 → Oracle 공식 문서
- Java GC 내부 동작 → JVM 공식 문서
'정리' 카테고리의 다른 글
| URL 입력부터 페이지 렌더링까지 (1) | 2025.04.07 |
|---|---|
| 포트포워딩, TCP/IP (0) | 2025.03.19 |
| Thread (0) | 2025.03.05 |
| JVM (0) | 2025.02.26 |
| REST API (0) | 2025.02.25 |