C#의 경우 메모리를 자동으로 정리해 주기 때문에 할당 및 해제를 크게 신경 쓰지 않는 경우가 많습니다. 하지만 자동으로 메모리 할당 및 해제를 해주는 가비지 컬렉션을 이해하지 못하고 코딩을 할 경우 최적화에 치명적인 코드를 작성할 수도 있습니다. 프로젝트 규모가 조금만 커져도 이것을 모르는 것과 아는 것은 천지이기 때문에 이번 시간에는 가비지 컬렉션을 이해하고 세대라는 것이 어떤 특징을 가졌는지 알아보겠습니다.
목차
[가비지 컬렉션 이란?]
메모리 할당과 해제를 관리해주고, 프로그램에서 더 이상 필요하지 않다고 판단되면 자동으로 메모리를 해제 시켜주는 프로세스 입니다.
[특징]
메모리를 자동 관리해주지 않는 프로그램의 경우 그것을 관리하지 않으면 메모리 누수가 생깁니다. 메모리 누수란 사용하지 않음에도 불구하고 관리하지 않아서 메모리를 계속 사용하고 있는 것을 말합니다.
이러한 메모리를 관리하기 위해서 C#의 가비지 컬렉션은 참조되지 않은 객체를 찾아 냅니다. 하나의 객체가 어느 곳에도 참조되지 않은 상태라면 자동으로 지우는 것 입니다. 이것 말고도 몇가지 특징을 더 알아 보겠습니다.
- 특정한 조건이 발동되면 진행 중인 쓰레드를 중지하고 가비지 컬렉션의 쓰레드를 실행 시킵니다.
- 가비지 컬렉션은 사용하지 않은 메모리를 삭제한 후 사용중인 객체의 위치를 재조정 시킵니다.
- 이것을 메모리 컴팩션이라고 하는데, 메모리공간이 이어지지 않고 단편화 되는 현상을 방지하는 것입니다.
![[C#] 가비지 컬렉션(Garbage collection)세대별 동작 및 메모리 해제 2 메모리컴팩션](https://programmingdev.com/wp-content/uploads/2023/09/메모리컴팩션-optimized.jpg)
첫번째 그림과 같이 가비지 컬렉션이 메모리를 정리하게 되면 빈공간이 발생하게 되는데 메모리 위치를 재조정해서 공간을 효율적으로 사용하기 위함 입니다.
3. 객체의 참조를 찾기 위해서는 객체 참조 그래프가 필요한데, 이 그래프를 생성하기 위해서는 루트 참조가 필요합니다.
루트 참조에 해당되는 것들은 메서드의 스택 변수(지역 변수 등), CPU 레지스터 변수가 가지고 있는 참조, 현재 사용하고 있는 각 클래스의 정적 필드(변수와 메서드), 전역 변수가 이에 해당 됩니다.
루트 참조가 참조하는 객체들을 추적하여 객체 참조 그래프를 완성 시키는 것입니다. 즉, 루트 참조는 모든 참조의 기준이 되는 참조인 것 입니다.
4. 객체 참조 그래프에 해당되지 않으면 쓰레기에 해당 됩니다. 쓰레기에 해당된 객체들은 모두 정리의 대상이 되는 것 입니다.
5. 언제 실행될지는 정확히 예측하기 어렵습니다. 평균적으로 임계치가 넘어가면 실행되지만, 이 순간 말고도 여러 상황에 따라서 실행되기 때문에 예측하기 쉽지 않습니다.
[세대 별 메모리 해제 과정]
메모리 공간은 0세대부터 2세대까지로 나뉘게 됩니다. 즉, 어떠한 목적에 의해서 나누어진 메모리 공간이라고 이해하시면 됩니다. 이제부터 그 목적과 세대 별 동작 과정을 알아보겠습니다.
먼저, 메모리가 부족하게 되면 0세대부터 차례로 검사하여 메모리를 해제하기 시작합니다. 0세대에는 이제 막 할당 된 객체들이 일정한 크기를 도달하면 옮겨지는 공간입니다. 그렇게 해제되고 남은 객체는 1세대로 옮겨지게 됩니다. 마찬가지로 1세대의 객체들이 임계치를 달성하게 되면 0세대와 같은 메모리 검사를 받아서 해제되게 됩니다. 그리고 해제되지 않은 객체들은 다시 2세대로 옮겨지는 것 입니다.
만약 2세대의 임계치가 차게 되면 어떻게 될까요? 게임이 잠시 멈춰 버리게 됩니다. 긴 시간은 아니지만 일종의 렉처럼 보일 가능성이 있습니다. 왜 이러한 현상이 생기게 되는지 설명 하겠습니다.
2세대의 임계치가 차게 되면 CLR이 실행파일의 실행을 멈춰버립니다. 그리고 가비지 컬렉션을 수행하게 되죠. 0세대부터 2세대 까지 모두 실행하기에 차지하고 있던 메모리가 클수록 멈춰 있는 시간이 길어지게 됩니다. (여기서 CLR은 공용 언어 런타임의 약자로 런타임 환경을 제공해주는 서비스 입니다.)
[코드 작성 시 주의사항]
객체를 너무 많이 할당하시면 안됩니다. 메모리의 임계치가 넘어가게 되면 가비지 컬렉션의 호출 수가 많아지게 되고, 호출 수가 많다는 것은 성능의 지장을 준다는 것 입니다.
또한 복잡한 참조 관계를 자제하셔야 합니다. 가비지 컬렉션은 객체의 참조를 주기적으로 조사하다 보니 복잡한 참조 관계가 형성 되면 오버헤드가 클 수 있습니다. 그리고 루트를 너무 많이 만드는 것을 자제하셔야 합니다. 여기서 루트는 아까 위 에서 설명한 것 입니다. 가지비 컬렉션은 루트를 돌면서 쓰레기를 찾아내는데 개수가 많아지게 되면 자연스럽게 프로그램의 성능도 떨어지게 되겠죠.
[가바지 컬렉션 장점]
- 해제된 메모리의 접근을 방지해 줍니다.
- 해제된 메모리를 다시 해제하는 것을 방지해 줍니다.
- 메모리 누수걱정이 줄어듭니다.
- 메모리 관리를 자동으로 해주기 때문에 개발 생산성이 향상 됩니다.
- 코드가 간결해 집니다. 메모리 관리 코드를 따로 작성하지 않아도 되니 자연스럽게 코드가 줄어드는 것 입니다.
[가비지 컬렉션 단점]
- 가비지 컬렉션이 일어나는 시점이나 점유 시간의 예측이 힘듭니다.
- 가비지 컬렉션 실행으로 인한 오버헤드가 발생합니다. 주기적으로 실행되는 특징으로 인한 단점입니다.
- 불필요한 메모리 사용. 자동으로 메모리를 관리 해 준다는 것은 정확한 시점의 해제하는 것이 어렵다는 뜻이기도 합니다. 그렇기 때문에 때로는 불필요하게 메모리 공간을 점유하는 상황도 발생하게 됩니다.
[오브젝트 풀링 사용 시 주의사항]
게임 개발에서 오브젝트 풀링은 자주 사용되는 디자인 패턴입니다. 만약 오브젝트 풀링을 모르신다면 여기를 클릭하셔서 보는 것을 추천드립니다.
오브젝트 풀링은 객체를 재활용 하기 때문에 세대 별 임계치의 도달하는 것을 막을 수 있지만 반대로 너무 많은 객체를 할당하게 되면 2세대 까지의 임계치가 빨리 차버려서 렉을 유발하는 현상이 생길 수 있습니다. 그렇기에 너무 많이 남발하면 안되며, 적정선을 잘 파악하셔야 합니다.
[마무리]
c#에서 메모리 관리는 자동으로 되기 때문에 신경을 안 쓰시는 분들이 많을 텐데 오히려 자동으로 해주기 때문에 더욱 신경 쓰셔야 합니다. 자동으로 해준다는 것은 사용자가 지정할 수 없다는 뜻이기에 가비지 컬렉션의 호출을 최소화하는 노력이 필요하다는 뜻이기도 합니다. 이 글을 여러 번 읽어 보시고 호출을 최소화 하여 성능이 최적화된 프로그램을 만들길 랍니다.
[참고한 사이트]
https://wordrow.kr/%EC%9D%98%EB%AF%B8/%EB%A9%94%EB%AA%A8%EB%A6%AC%20%EC%BB%B4%ED%8C%A9%EC%85%98/
https://sting1219.blogspot.com/2019/11/c-garbage-collection.html