⭐ 메모리
컴퓨터에서 메모리는 프로그램이 실행될 때 데이터를 저장하고 읽는 임시 공간입니다.
프로그램이 동작하기 위해 필요한 변수, 객체, 함수 코드, 버퍼 등 대부분의 정보가 메모리 공간에 할당됩니다.
메모리는 대표적으로 스택, 힙, 데이터, 코드 메모리로 나누어집니다.
단, 스택과 힙은 실행 환경이 바뀌어도 거의 있지만 데이터와 같은 영역은 실행 환경에 따라 다르기 때문에 스택, 힙 위주로 알아두는게 좋습니다.
이어서 각 메모리가 어떤 역할을 하고, 데이터를 할당받는지 알아보겠습니다.
스택 (stack) 메모리
○ 스택 메모리는 함수 호출, 지역변수, 매개변수, 값 형식 데이터 등을 저장하는 메모리 영역입니다.
○ 함수 호출 시 스택 프레임이 메모리에 저장되며, 함수 호출이 종료되면 해당 데이터를 메모리에서 해제합니다.
함수 호출 시 스택 메모리 과정
함수가 호출되면 해당 함수의 변수, 반환 주소값 등이 담겨있는 스택프레임이라는 함수의 정보가 스택 메모리에 담기게 됩니다.
함수의 호출이 종료될 시점에 해당 함수의 스택프레임을 스택 메모리에서 알아서 해제합니다.
힙 (heap) 메모리
○ 힙 메모리는 보통 참조 형식 데이터를 저장하는 메모리 영역입니다.
○ 동적을 할당되는 데이터와 객체(클래스), 참조타입, 박싱(boxing)된 값 타입 등이 저장됩니다.
○ C#에서 힙 메모리에 저장된 데이터들은 GC(가비지 컬렉터)에 의해 자동으로 관리됩니다.
○ 힙에 실 데이터를 저장하고 스택 메모리에선 힙 영역에 있는 데이터를 참조할 수 있는 주소값이 저장됩니다.
힙 메모리 저장 예시
클래스나 참조 타입 등의 데이터 값이 힙 메모리에 저장되고 해당 주소를 스택 메모리에 저장해 접근합니다.
코드 (code) 메모리
○ 프로그램의 실행 가능한 명령어들을 저장하는 메모리 영역입니다.
○ 컴파일된 C# 코드가 기계어로 변환되어 코드 메모리 영역에 로드됩니다.
○ 프로그램의 실행될 때 CPU는 이 영역에서 명령어를 가져와 순차적으로 실행합니다.
○ 코드 메모리 영역은 일반적으로 읽기 전용으로 취급되어 프로그램 실행 중에 수정되지 않습니다.
데이터 (data) 메모리
○ 전역 변수나, 정적 변수처럼 프로그램의 수명 전체에 걸쳐 존재해야 하는 데이터가 저장되는 메모리 영역입니다.
○ 데이터 메모리 영역은 크게 두 가지 영역으로 나뉩니다.
▶ 초기화된 데이터 영역 (.data): 이미 초기값이 할당된 전역/정적 변수가 저장됩니다.
▶ 초기화되지 않은 데이터 영역 (.bss): 초기값이 명시적으로 할당되지 않은 전역/정적 변수가 저장되며,
프로그램 시작 시 0 또는 null로 자동 초기화됩니다.
⭐ C / C++ / C# 에서의 메모리 관리
C와 C++의 경우 힙(heap) 메모리 영역을 개발자가 직접 관리해줘야 합니다.
malloc / new로 할당하고 delete 등의 키워드로 직접 해제해야 합니다.
C#의 경우 GC(가비지컬렉터)라고 불리는 시스템이 자동으로 동적 메모리를 관리합니다.
Garbage Collector (가비지 컬렉터) 란?
가비지 컬렉터란 메모리의 할당과 해제를 자동으로 관리해주는 시스템입니다.
더 이상 사용하지 않는 객체를 찾아 자동으로 메모리에서 해제해주기 때문에 사용자가 직접 메모리를 해제할 필요가 없습니다.
하지만 가비지 컬렉터 호출 시 다른 스레드들을 일시정지하고 실행하기 때문에 너무 잦은 가비지 컬렉터 호출은 성능 저하를 불러올 수 있습니다.
💡 가비지 컬렉터 동작 원리
가비지 컬렉터는 힙 메모리에서 더 이상 사용되지 않는 객체를 자동을 해제하는 역할을 합니다.
이 과정은 일반적으로 크게 세가지 단계로 이루어집니다.
1. 마킹 (Marking): GC는 먼저 루트 오브젝트에서 시작하여 모든 객체를 추적합니다. 루트 오브젝트는 스택에 있는 변수들, CPU 레지스터, 정적 변수 등을 포함합니다. GC는 이 루트에서 따라가면서 접근 가능한 모든 객체들을 마킹(표시)합니다.
마킹되지 않은 객체들은 죽은 객체로 간주합니다.
= 데이터들에 마킹하는 단계
2 스위핑 (Sweeping): 마킹 단계가 끝나면, GC는 힙 메모리를 한 바퀴 돌면서 마킹되지 않은 죽은 객체들을 제거하여 힙 메모리 공간을 회수합니다.
= 마킹되지 않은 (사용하지 않는) 객체를 청소하는 단계
3. 컴펙션 (Compaction): 메모리를 회수하는 과정에서 힙 메모리에 조그마한 빈 공간이 생길 수 있습니다.
GC는 살아있는 객체들을 시작점으로 이동시켜 빈 공간을 하나로 합칩니다. 이를 통해 메모리 단편화를 줄일 수 있습니다.
= 청소 후 힙 메모리 공간을 압축하는 단계
💡 가비지 컬렉션
가비지 컬렉션은 총 3개의 세대로 구성되어 관리됩니다.
대부분의 객체는 수명이 짧기 때문에 0세대에서 빠르게 수집되며, 오랫동안 살아남은 객체는 더 높은 세대로 승격됩니다.
● 0세대: 가장 어린 세대로, 새로 생성된 모든 객체가 이곳에 할당됩니다. 대부분의 객체는 수명이 짧아 0세대에서 빠르게 수집됩니다. GC는 0세대에서 가장 자주 발생하며, 0세대에서 살아남은 객체는 1세대로 승격됩니다.
● 1세대: 0세대와 2세대의 중간 버퍼 역할을 합니다.0세대에서 한 번의 컬렉션을 버텨낸 객체들이 이곳으로 이동합니다.
1세대에서 살아남은 객체는 2세대로 승격됩니다.
● 2세대: 가장 오래된 객체들이 모이는 곳으로, 프로그램의 수명과 거의 비슷한 긴 수명을 가진 객체들이 존재합니다.
2세대 GC는 전체 힙을 대상으로 발생하므로 전체GC 라고도 불리며, 가장 비용이 많이 들고 가장 드물게 발생합니다.
● Large Object Heap (LOH): 크기가 대략 85,000바이트를 초과하는 대용량 객체는 0세대를 거치지 않고 바로 LOH에 할당됩니다. LOH는 논리적으로 2세대의 일부로 취급되며, 2세대 GC가 발생할 때 함께 수집됩니다.
LOH 공간이 따로 있는 이유는 대용량 객체가 자주 할당되고 해제되는 것을 방지하여 메모리 단편화를 줄이기 위함입니다.
동일한 이유로 LOH는 압축이 기본적으로 비활성화되어 있습니다.
가비지 컬렉터 호출 과정
1. 객체 할당: 새로 생성된 객체를 0세대에 할당합니다. 새로운 객체가 할당될 공간이 부족해지면 가장 먼저 0세대 GC가 실행됩니다.
2. 0세대 GC: 0세대 GC에서 살아남은 객체들은 1세대로 승격됩니다. 살아남지 못한 객체들은 수집되어 메모리에서 해제됩니다.
3. 1세대 GC: 0세대 GC가 실행되었음에도 불구하고 메모리 공간이 충분히 확보되지 않거나, 1세대 공간이 가득 차면 1세대 GC가 실행됩니다. 마찬가지로 1세대 GC에서 살아남은 객체는 2세대로, 살아남지 못한 객체는 메모리에서 해제됩니다.
4. 2세대 GC: 2세대 공간이 가득차거나 1세대 GC로도 메모리 공간이 부족할 경우 실행됩니다.
2세대는 모든 세대를 포함하여 전체 힙 메모리 대상으로 실행되며, 이를 전체 GC라고 부릅니다. 이 과정은 가장 비용이 많이 들고 시간이 오래 걸립니다.
5. Full GC: GC 중에서 가장 비용이 많이 드는 프로세스입니다. 이는 모든 세대(LOH 포함)를 포함한 모든 힙 영역을 대상으로 수행됩니다. Full GC는 메모리가 부족하거나 명시적으로 호출되었을 경우 실행됩니다.
💡 가비지 컬렉터의 특징
1. 자동 메모리 회수: 개발자가 직접 관리하지 않아도 자동으로 관리됩니다.
2. 참조 기반 판단: 객체에 오랫동안 아무런 참조가 없던 경우 쓰이지 않는다고 판단 후 회수합니다.
3. 주기적 실행: 일정 시간마다, 또는 메모리가 부족할 경우 실행됩니다.
💡 가비지 컬렉터의 장/단점
장점
1. 메모리 관리 자동화: 개발자가 수동으로 메모리를 할당하고 해제하는 번거로움을 덜어줍니다.
2. 개발 생산성 향상: 메모리 관리에 대한 부담이 줄어들어 개발자가 로직 구현에 더 집중할 수 있게 됩니다.
3. 안전성 향상: 잘못된 메모리 접근 오류로부터 프로그램을 보호하여 안정성을 높입니다.
4. 메모리 단편화 감소: 가비지 컬렉션 과정에서 메모리를 재배치하고 압축하여 메모리 단편화를 줄입니다.
5. 메모리 누수 방지: 객체가 더 이상 필요하지 않은 시점을 판단하여 자동으로 메모리를 해제해주기 때문에 메모리 누수를 방지할 수 있습니다.
단점
1. 예측 불가능한 실행 시점: GC가 언제 발생할지 예측하기 어렵기 때문에 실시간 게임이나 시스템에서 순간적인 프레임 드랍이 발생할 수 있습니다.
2. 성능 비용: GC는 실행 시 메모리를 탐색하며 이 과정에서 비용이 생기게 됩니다. 이는 높은 세대일 수록 성능에 큰 영향을 미칩니다.
3. 메모리 사용량 증가: GC는 즉시 메모리를 해제하지 않고 참조가 사라질 때까지 남겨두는 경우가 있습니다. 이 경우 필요 이상으로 메모리를 점유할 수도 있습니다.
4. 최적화를 위해선 직접 관리가 필요: GC는 자동으로 실행되는 시스템이지만, 발생 시 비용이 발생하기 때문에 메모리 프로파일링 및 최적화 설계가 필요합니다.
가비지 컬렉션 최적화 방법
1. 불필요한 객체 할당 최소화: 객체를 생성하는 것은 가비지 컬렉션의 부담을 증가시킵니다. 코드를 최적화하고 필요한 경우에만 객체를 생성해야 합니다.
2. 객체 재사용: 동일한 객체를 여러번 생성하기보다, 이미 생성된 객체를 재사용해서 메모리 할당 횟수를 줄입니다.
3. 구조체 사용: 작은 데이터 구조는 객체(Class) 대신에 구조체를 사용하여 힙이 아닌 스택에 할당되도록 하여 가비지 컬렉션의 부하를 줄일 수 있습니다.
4. 큰 객체 관리: 큰 객체는 힙에 할당되며, 크기가 대략 85,000byte가 넘는 객체는 LOH로 이동하여 별도로 관리됩니다.
이 LOH는 가비지 컬렉션의 효율을 떨어트릴 수 있으므로, 큰 객체의 생성을 최소화하거나 관리 방법을 따로 고려해야 합니다.
5. 최적화된 알고리즘 및 데이터 구조 사용: 알고리즘 자체를 최적화하여, 메모리 할당량을 줄입니다.
'개발, IT > C#' 카테고리의 다른 글
| [유니티 / C#] 클래스의 상속(Inheritance) (0) | 2025.09.18 |
|---|---|
| [유니티 / C#] static (정적) 한정자 (0) | 2025.09.17 |
| [유니티 / C#] 클래스(Class) (0) | 2025.09.16 |
| [유니티 / C#] 프로퍼티 (Property) (0) | 2025.09.15 |
| [유니티 / C#] 객체 지향 프로그래밍과 SOLID 원칙 (0) | 2025.09.15 |


