*참고한 공식 문서
https://learn.microsoft.com/ko-kr/dotnet/standard/garbage-collection/fundamentals
가비지 컬렉션 기본 사항 - .NET
가비지 수집기의 작동 원리와 최적 성능으로 구성하는 방법에 대해 알아봅니다.
learn.microsoft.com
https://learn.microsoft.com/ko-kr/dotnet/standard/garbage-collection/large-object-heap
Windows의 큰 개체 힙 (LOH) - .NET
이 문서에서는 큰 개체, .NET 가비지 수집기에서 관리하는 방법 및 큰 개체를 사용하는 성능에 미치는 영향에 대해 설명합니다.
learn.microsoft.com
백그라운드 가비지 수집 - .NET
.NET의 백그라운드 가비지 수집 및 워크스테이션 및 서버 가비지 수집의 차이점에 대해 알아봅니다.
learn.microsoft.com
https://docs.unity3d.com/Manual/performance-reference-types.html#Garbage-collector
https://docs.unity3d.com/Manual/scripting-backends-il2cpp.html
A garbage collector for C and C++
A garbage collector for C and C++ [ This is an updated version of the page formerly at http://www.hpl.hp.com/personal/Hans_Boehm/gc, and before that at http://reality.sgi.com/boehm/gc.html and before that at ftp://parcftp.xerox.com/pub/gc/gc.html. ] The Bo
www.hboehm.info
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
Java Garbage Collection Basics
Java Overview Java is a programming language and computing platform first released by Sun Microsystems in 1995. It is the underlying technology that powers Java programs including utilities, games, and business applications. Java runs on more than 850 mill
www.oracle.com
https://docs.oracle.com/en/java/javase/17/gctuning/index.html
HotSpot Virtual Machine Garbage Collection Tuning Guide
This guide describes the garbage collection methods included in the Java HotSpot Virtual Machine (Java HotSpot VM) and helps you determine which one is the best for your needs.
docs.oracle.com
* Garbage Collector(GC)
[핵심 목표] 더 이상 사용하지 않는 객체의 메모리를 자동으로 찾아서 해제한다
* 동작 단계
.NET의 GC는 기본적으로 Mark → Sweep → Compact 패턴을 따른다
(1) Mark 단계 - 사용중인 객체를 표시
실행 중인 애플리케이션의 루트(Root) 객체부터 시작해 참조 체인을 따라 살아있는 객체(alive)로 표시한다
(2) Sweep 단계 - 사용하지 않는 객체를 제거
Mark 단계에서 표시되지 않는 객체를 가비지(쓰레기)로 간주하고, 그 메모리를 해제한다
[단어 뜻] 쓸다, 청소하다
(3) Compact 단계 - 살아 남은 객체를 모아 연속된 공간을 만든다
Sweep 후 생긴 메모리 빈 공간을 정리하고,
살아남은 객체들을 한쪽으로 모아 재배치(compaction)해 메모리 파편화를 줄인다
* .NET GC는 비용 절감을 위해 세대별로 나눠서 효율적으로 관리한다
[구현 요점] 일반적으로 젊은 객체는 빨리 수집되고, 오래 살아남은 객체는 높은 세대로 승격되어 더 오래 유지된다
이름 | 특징 | 대상 | 청소 빈도 |
Generation0 ( = Gen0) |
새로 만들어진 ‘젊은’ 객체 | 단기 객체 (대부분 메서드 로컬 변수 등 임시 객체) |
가장 자주 수집 |
Generation1 ( = Gen1) |
Gen0에서 살아남은 객체 | 중간 생존 객체 | 중간 빈도로 수집 |
Generation2 ( = Gen2) |
Gen1에서 살아남은 객체 | 장기 생존 객체 (싱글턴, 앱 캐시 등 장기 리소스) |
비용이 가장 크고 가장 늦게 수집 |
LOH (Large Object Heap) |
85,000바이트 이상 대규모 객체 | Gen2에서 관리, 단편화 발생 가능, 기본 압축 안함 |
Gen2 수집 시 함께 수집 |
* .NET GC에서 세대별 관리하는 동작 원리
(1) 할당(Allocation)
객체가 생성되면 메모리는 관리 힙(Managed Heap)에 할당되며, Gen0 영역에 배치된다
대부분의 객체는 메서드 로컬 변수로 생성되어 짧게 사용된다
(2) GC 실행: Mark → Sweep → Compact
[2-1] 실행되는 조건
조건 | 설명 |
Gen0/Gen1 힙이 꽉 참 | 세대별로 설정된 최대 메모리를 모두 사용했을 때 |
OS가 Low Memory 신호 | 컴퓨터 메모리가 부족해졌을 때 |
명시적 호출 | 개발자가 GC.Collect()를 직접 호출한 경우 |
Background GC 주기 | Gen2는 백그라운드 스레드가 주기적으로 검사하여 필요하면 작동 |
할당 속도가 예상보다 빠름 → Budget 초과 위험 |
객체 생성 속도가 빨라 곧 메모리가 모자랄 것으로 예상될 때 |
[2-2] Allocation Budget 원리
.NET은 각 세대별로 객체를 만들 수 있는 최대 용량(예산)을 정한다
새 객체를 만들면 그만큼 공간을 차지해 예산이 줄어든다
예산을 모두 쓰면 사용하지 않는 객체를 제거하여 공간을 비운다
최근 객체가 만드는 속도가 너무 빨라서 곧 예산을 다 쓸 것 같으면, 미리 GC를 실행해 부족하지 않게 한다
[2-3] GC 검사 순서
작은 영역부터 순서대로 점검(Gen0 → Gen1)
Gen0만 정리해도 충분하면 상위 세대는 건너뛰어 비용을 절감한다
Gen2는 백그라운드에서 주기적으로 살펴보고 필요할 때 오래된 객체를 정리한다
(3) 승격(Promotion)
Get0에서 살아남은 객체는 Get1으로 승격
Get1에서 살아남은 객체는 Get2로 승격
* ChatGPT로 만들고, 간단하게 리펙토링한 GC 예제 코드
namespace GarbageCollector
{
// 실제 메모리에 할당된 객체를 표현하는 단위
class GcObject
{
private static int _idCount = 1;
private readonly List<GcObject> _references = new(); // 해당 객체가 참조하는 다른 객체 리스트, 참조하는 객체가 없으면 GC 대상이 된다
private int _id;
private bool _isMarked; // 사용 여부 체크
public GcObject()
{
_id = _idCount;
_idCount++;
}
public int GetId() => _id;
public bool IsMarked()
{
return _isMarked;
}
public void Mark()
{
_isMarked = true;
}
public void Unmark()
{
_isMarked = false;
}
public List<GcObject> GetReferences()
{
return _references;
}
public void AddReferences(GcObject reference)
{
_references.Add(reference);
}
public void ClearReferences()
{
_references.Clear();
}
}
class SimpleGc
{
private readonly List<GcObject> _roots = new();
private List<GcObject> _heap = new();
// 루트부터 연결된 객체들을 Mark
void Mark()
{
// DFS(깊이 우선 탐색)으로 참조된 모든 객체까지 전부 검사
var stack = new Stack<GcObject>(_roots);
while (stack.Count > 0)
{
var obj = stack.Pop();
if (!obj.IsMarked())
{
obj.Mark();
foreach (var reference in obj.GetReferences())
stack.Push(reference);
}
}
}
// Mark 안 된 객체 제거, Sweep
void Sweep()
{
_heap.RemoveAll(obj =>
{
// 힙에서 제거, true 리턴하면 제거됨
if (!obj.IsMarked())
{
Console.WriteLine($"[SWEEP] Sweeping Obj#{obj.GetId()}");
return true;
}
// 힙에서 유지, false 리턴하면 유지
obj.Unmark(); // 다음 GC를 위해 _isMarked값 초기화
return false;
});
}
// Compact
void Compact()
{
// 진짜 Compact처럼 메모리 재배치하지는 않지만,
// 리스트를 새로 만들어 정리된 것 같은 효과만 구현
var newHeap = new List<GcObject>(_heap);
_heap = newHeap;
Console.WriteLine("[COMPACT] Heap compacted. Count: " + _heap.Count);
}
// GC 한 사이클 실행
public void RunGc()
{
Console.WriteLine("=== GC Start ===");
Mark();
Sweep();
Compact();
Console.WriteLine("=== GC End ===\n");
}
public void AddHeap(GcObject[] insertData)
{
_heap.AddRange(insertData);
}
public void SetRoot(GcObject root)
{
_roots.Add(root);
}
}
internal class Program
{
static void Main(string[] args)
{
var gc = new SimpleGc();
// 객체 생성
var obj1 = new GcObject();
var obj2 = new GcObject();
var obj3 = new GcObject();
// 참조 관계 생성
// obj1 → obj2 → obj3
obj1.AddReferences(obj2);
obj2.AddReferences(obj3);
gc.AddHeap(new [] { obj1, obj2, obj3 });
// obj1만 Root에 등록 → obj2, obj3는 obj1만 통해 접근됨
gc.SetRoot(obj1);
Console.WriteLine("[STEP 1] 모두 연결됨");
gc.RunGc();
// obj1에서 obj2로의 연결 끊기 → obj2, obj3는 더 이상 접근 불가
obj1.ClearReferences();
Console.WriteLine("[STEP 2] obj1 → obj2 연결 해제됨");
gc.RunGc();
}
}
}
* 유니티의 GC
런타임(Mono, IL2CPP)에 따라 동작 방식이 다르다
[Mono]
기본적으로 Boehm GC를 사용하며, 옵션으로 Incremental 모드를 켤 수 있다
ㄴ Boehm GC
세대별 구조(generational GC)와는 달리, 모든 힙 영역을 한꺼번에 탐색한다
세대별 구조와 동일하게 Mark → Sweep → Compact 패턴으로 메모리 관리한다
ㄴ Incremental GC 옵션
GC 작업을 여러 프레임에 나누어 점진적으로 수행하여,
코루틴처럼 작은 단위로 실행되어 일시적인 프레임 드롭을 줄인다
[IL2CPP]
AOT(Ahead Of Time) 컴파일 환경에 맞게 Boehm GC를 내장하여 사용하며,
Mono와 동일하게 기본적으로 Boehm GC 기반으로 작동한다
Incremental GC는 IL2CPP에서는 직접 설정할 수 없고,
플랫폼과 빌드 설정에 따라 내부에서 자동으로 Boehm GC가 최적화되어 동작한다
[정리하면]
결국 Boehm GC를 사용하고, Incremental 모드를 사용할 수 있다는 점은 같다
단, Mono는 Incremental 모드를 선택할 수 있지만 IL2CPP는 내부에서 자동으로 적용된다
* AOT와 JIT 언급한 유니티 공식문서에서의 문구
This type of compilation,
in which Unity compiles code specifically for a target platform when it builds the native binary,
is called ahead-of-time (AOT) compilation.
The Mono backend compiles code at runtime,
with a technique called just-in-time compilation (JIT).
* 자바의 GC
기본적으로 세대별 GC(generational GC) 구조를 사용한다
[기본구조]
객체의 생존 기간에 따라
Young Generation, Old Generation, Permanent Generation으로 나눈다
ㄴ Young Generation
새로 생성된 객체가 주로 할당
ㄴ Old Generation
Young Generation에서 오래 살아남은 객체가 이동
ㄴ Permanent Generation(Java7까지) → Metaspace(Java8부터)
클래스 메타데이터 등을 저장
Minor GC(eden + Survivor Space)
Young Generation(Eden + Survivor Space)만 대상으로 하는 부분 수집
짧고 자주 발생하며, 살아남은 객체는 Survivor Space를 거쳐 Old Generation으로 이동
Old Generation이 차면 Major GC가 발생하고, 필요 시 힙 전체를 대상으로 Full GC가 수행된다
Major GC
Old Generation을 대상으로 하는 큰 수집
필요 시 힙 전체를 대상으로 Full GC가 수행된다
Full GC
JVM이 관리하는 모든 힙 전체(Young + Old + Metaspace)를 수집
Minor, Major보다 훨씬 무겁고 Stop-The-World(전체 멈춤) 시간도 길 수 있다
성능 문제의 주 원인으로 꼽히므로 자주 발생하지 않도록 튜닝하는 게 중요하다
[HotSpot JVM이란]
Oracle이 만든 대표적인 Java Virtual Machine(JVM) 구현체이다
[대표 GC 알고리즘]
HotSpot JVM에서는 여러 GC 알고리즘을 선택할 수 있다
(1) Serial GC
: 단일 스레드로 작동하는 단순한 GC
(2) Parallel GC
: 여러 스레드로 Young Generation을 병렬로 수집
(3) CMS GC (Concurrent Mark-Sweep)
: Old Generation을 애플리케이션과 동시에 수집하여 Stop-The-World(STW) 시간을 줄임
(4) G1 GC (Garbage First)
: 힙을 작은 Region으로 나누고 우선순위가 높은 영역부터 수집
(5) ZGC, Shenandoah GC
: 매우 짧은 지연시간을 목표로 하는 최신 GC, Java 11+에서 사용 가능
'IT공부' 카테고리의 다른 글
[C#] .NET 9 Random 클래스 살펴보기 (0) | 2025.06.18 |
---|---|
[C#] .NET 9 decimal 구조체 톺아보기 (0) | 2025.06.16 |
[C#, Unity] 비동기 함수와 코루틴의 차이 (0) | 2025.05.05 |
[Unity] 왜 .cs 파일을 스크립트 파일이라고 부를까? 그리고 인터프리터 언어인가? (0) | 2025.04.27 |
[C#] 추상 클래스와 인터페이스의 차이 (0) | 2025.04.14 |