콘텐츠로 이동

Unity 메모리 & GC 최적화

Unity 게임에서 GC 스파이크는 프레임 드롭의 주요 원인 중 하나입니다. Mono와 IL2CPP 모두 Boehm GC(비세대적, Stop-The-World)를 기반으로 하며, 힙 할당이 누적되면 GC가 실행되는 시점에 수십 밀리초의 멈춤이 발생합니다.


Unity는 두 가지 스크립팅 백엔드를 지원합니다.

항목MonoIL2CPP
컴파일 방식JIT (런타임)AOT (빌드 시 C++로 변환)
GC 방식Boehm GCBoehm GC (동일)
성능개발 빌드에 적합배포 빌드 권장
플랫폼 지원PC·에디터iOS·콘솔 필수

GC가 실행되면 모든 관리형 스레드가 일시 정지됩니다 (Stop-The-World). 힙 할당을 줄이는 것이 근본적인 해결책입니다.


// ❌ int → object boxing 발생
object boxed = 42;
Dictionary<string, object> data = new();
data["hp"] = 100; // 매 프레임 boxing
// ✅ 제네릭 사용
Dictionary<string, int> data = new();
data["hp"] = 100;
// ❌ 매 프레임 새 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();
}
// ❌ 매 호출 시 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]);
// ❌ 클로저 캡처 시 힙에 클래스 인스턴스 생성
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 });
// ❌ 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) { }

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)를 호출합니다.


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; // 3ms

Incremental GC는 스파이크를 완전히 없애지 않습니다. 근본 해결은 할당 자체를 줄이는 것입니다.


Unity Memory Profiler 패키지를 설치하면 힙 스냅샷을 찍어 분석할 수 있습니다.

Window → Memory Profiler → Capture New Snapshot
분석 항목확인 내용
Managed Heap전체 힙 크기 및 단편화
Top Objects by Size메모리를 가장 많이 사용하는 객체
Duplicated Assets중복 로드된 텍스처·메시
Native ObjectsRenderTexture·AudioClip 등

증상원인해결
Update마다 0.1~0.5KB 할당string + 또는 new ListStringBuilder / 풀 사용
씬 전환 시 수백 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이 발생할 수 있습니다.

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();
}
}