Unity 메모리 & GC 최적화
Unity 게임에서 GC 스파이크는 프레임 드롭의 주요 원인 중 하나입니다. Mono와 IL2CPP 모두 Boehm GC(비세대적, Stop-The-World)를 기반으로 하며, 힙 할당이 누적되면 GC가 실행되는 시점에 수십 밀리초의 멈춤이 발생합니다.
GC 동작 원리
섹션 제목: “GC 동작 원리”Unity는 두 가지 스크립팅 백엔드를 지원합니다.
| 항목 | Mono | IL2CPP |
|---|---|---|
| 컴파일 방식 | JIT (런타임) | AOT (빌드 시 C++로 변환) |
| GC 방식 | Boehm GC | Boehm GC (동일) |
| 성능 | 개발 빌드에 적합 | 배포 빌드 권장 |
| 플랫폼 지원 | PC·에디터 | iOS·콘솔 필수 |
GC가 실행되면 모든 관리형 스레드가 일시 정지됩니다 (Stop-The-World). 힙 할당을 줄이는 것이 근본적인 해결책입니다.
GC Alloc 주요 원인
섹션 제목: “GC Alloc 주요 원인”1. Boxing
섹션 제목: “1. Boxing”// ❌ int → object boxing 발생object boxed = 42;Dictionary<string, object> data = new();data["hp"] = 100; // 매 프레임 boxing
// ✅ 제네릭 사용Dictionary<string, int> data = new();data["hp"] = 100;2. 문자열 연결
섹션 제목: “2. 문자열 연결”// ❌ 매 프레임 새 string 생성void Update(){ debugText.text = "HP: " + currentHp; // 16~32B 할당}
// ✅ StringBuilder 또는 TryFormat 사용readonly System.Text.StringBuilder _sb = new();
void Update(){ _sb.Clear(); _sb.Append("HP: "); _sb.Append(currentHp); debugText.text = _sb.ToString();}3. LINQ
섹션 제목: “3. LINQ”// ❌ 매 호출 시 IEnumerable 래퍼 할당var aliveEnemies = enemies.Where(e => e.IsAlive).ToList();
// ✅ for 루프로 대체var aliveEnemies = new List<Enemy>(enemies.Count);for (int i = 0; i < enemies.Count; i++) if (enemies[i].IsAlive) aliveEnemies.Add(enemies[i]);4. 클로저 (Lambda 캡처)
섹션 제목: “4. 클로저 (Lambda 캡처)”// ❌ 클로저 캡처 시 힙에 클래스 인스턴스 생성float threshold = 10f;enemies.Sort((a, b) => a.Distance(threshold).CompareTo(b.Distance(threshold)));
// ✅ IComparer 구조체로 할당 제거struct DistanceComparer : IComparer<Enemy>{ public float threshold; public int Compare(Enemy a, Enemy b) => a.Distance(threshold).CompareTo(b.Distance(threshold));}enemies.Sort(new DistanceComparer { threshold = 10f });5. foreach on non-generic collections
섹션 제목: “5. foreach on non-generic collections”// ❌ ArrayList foreach → IEnumerator 박싱ArrayList list = new ArrayList();foreach (object item in list) { }
// ✅ List<T> 사용List<Enemy> list = new List<Enemy>();foreach (Enemy e in list) { }Object Pooling
섹션 제목: “Object Pooling”Unity 2021부터 UnityEngine.Pool 네임스페이스에 공식 풀 API가 포함됩니다.
using UnityEngine;using UnityEngine.Pool;
public class BulletPool : MonoBehaviour{ [SerializeField] GameObject bulletPrefab;
IObjectPool<GameObject> _pool;
void Awake() { _pool = new ObjectPool<GameObject>( createFunc: () => Instantiate(bulletPrefab), actionOnGet: obj => obj.SetActive(true), actionOnRelease: obj => obj.SetActive(false), actionOnDestroy: Destroy, collectionCheck: false, // 중복 반환 체크 (에디터 디버깅용) defaultCapacity: 20, maxSize: 100 ); }
public GameObject Get() => _pool.Get(); public void Release(GameObject obj) => _pool.Release(obj);}총알이 화면 밖으로 나가거나 충돌하면 Destroy 대신 pool.Release(obj)를 호출합니다.
Incremental GC
섹션 제목: “Incremental GC”Unity 2019.1부터 Incremental GC를 지원합니다. Stop-The-World 대신 GC 작업을 여러 프레임에 분산합니다.
설정 방법:
Project Settings → Player → Configuration → Use Incremental GC 체크
// 스크립트에서 GC 시간 제한 설정 (ms)UnityEngine.Scripting.GarbageCollector.incrementalTimeSliceNanoseconds = 3_000_000; // 3msIncremental GC는 스파이크를 완전히 없애지 않습니다. 근본 해결은 할당 자체를 줄이는 것입니다.
Memory Profiler 활용
섹션 제목: “Memory Profiler 활용”Unity Memory Profiler 패키지를 설치하면 힙 스냅샷을 찍어 분석할 수 있습니다.
Window → Memory Profiler → Capture New Snapshot| 분석 항목 | 확인 내용 |
|---|---|
| Managed Heap | 전체 힙 크기 및 단편화 |
| Top Objects by Size | 메모리를 가장 많이 사용하는 객체 |
| Duplicated Assets | 중복 로드된 텍스처·메시 |
| Native Objects | RenderTexture·AudioClip 등 |
실무 GC 스파이크 원인과 해결
섹션 제목: “실무 GC 스파이크 원인과 해결”| 증상 | 원인 | 해결 |
|---|---|---|
| Update마다 0.1~0.5KB 할당 | string + 또는 new List | StringBuilder / 풀 사용 |
| 씬 전환 시 수백 MB 스파이크 | 이전 씬 오브젝트 미해제 | Resources.UnloadUnusedAssets() |
| GetComponent 매 프레임 호출 | 캐싱 누락 | Awake에서 캐싱 |
Coroutine yield return new WaitForSeconds | 매 실행마다 WaitForSeconds 인스턴스 생성 | 캐싱 static readonly WaitForSeconds |
// ❌ 매 코루틴 호출마다 할당yield return new WaitForSeconds(1f);
// ✅ 재사용static readonly WaitForSeconds Wait1Sec = new WaitForSeconds(1f);yield return Wait1Sec;GC 정지 없는 크리티컬 구간 — GC.TryStartNoGCRegion
섹션 제목: “GC 정지 없는 크리티컬 구간 — GC.TryStartNoGCRegion”보스 등장, 씬 전환 직전 등 GC 스파이크가 절대 발생해선 안 되는 구간에서 사용합니다.
public class BossSpawnSequence : MonoBehaviour{ public void StartSpawn() { // 8MB 예약 — 이 구간에서 힙 할당이 8MB를 초과하면 GC 발생 bool started = GC.TryStartNoGCRegion( totalSize: 8 * 1024 * 1024, disallowFullBlockingGC: false);
try { SpawnBoss(); PlayCutscene(); } finally { if (started) GC.EndNoGCRegion(); } }
private void SpawnBoss() { /* ... */ } private void PlayCutscene() { /* ... */ }}
disallowFullBlockingGC: true로 설정하면 예약 크기를 초과해도 GC를 실행하지 않지만, 메모리 부족 시OutOfMemoryException이 발생할 수 있습니다.
NativeArray로 비관리 메모리 활용
섹션 제목: “NativeArray로 비관리 메모리 활용”Job System과 함께 사용하지 않더라도 NativeArray는 GC 힙 외부에 데이터를 배치해 GC 부담을 줄일 수 있습니다.
using Unity.Collections;using Unity.Collections.LowLevel.Unsafe;
public class ParticleData : IDisposable{ private NativeArray<Vector3> _positions; private NativeArray<float> _lifetimes;
public ParticleData(int count) { // Allocator.Persistent: GC 힙 밖, 수동 해제 필요 _positions = new NativeArray<Vector3>(count, Allocator.Persistent); _lifetimes = new NativeArray<float>(count, Allocator.Persistent); }
public void Dispose() { if (_positions.IsCreated) _positions.Dispose(); if (_lifetimes.IsCreated) _lifetimes.Dispose(); }}