Memory
Garbage Collector
GC 의 기능 중에 하나는 메모리를 정리해서 Fragmentation 을 막는 것이 있다. 이를 효율적으로 하기위해 Genration 을 나누어 GC 빈도를 달리한다.
- 처음
new
로 생성된 객체는 Generation 0 이고GC.Collect()
에서 살아남으면 최대 Genration 2 까지 변한다. 이는GC.GetGeneration(obj)
함수를 통해 확인할 수 있다. - 메모리가 큰 객체는 LOH(Large Object Heap) 에 저장되어 GC 에도 메모리 주소를 유지시킨다. 또한 처음부터 Generation 2 가 된다. 그래서 Fragmentation 을 주의해야한다.
Memory 가 아닌 Resource 의 경우 할당 해제 시점이 중요한 경우가 많아 IDisposable
인터페이스를 이용해 명시적으로 해제한다. 보통 try
에서 자원을 획득하고 예외를 대비해 finally
에 Dispose()
를 호출하거나, 내부적으로 같은 역할인 using
문을 사용한다.
Finalizer
Finalizer 가 정의된 클래스의 객체는 GC 과정이 복잡하다.
- Managed Heap 에 들어갈 때 Finalizer Queue 에도 등록된다.
- 만약 객체가 더이상 참조되지 않는다면 Finalizer Queue 의 참조 때문에 살아남고, Finalizer Queue 에서 Freechable Queue 로 옮겨간다.
- Freechable Queue 에 항목이 들어올 때마다 호출되는 스레드가 Finalizer 를 호출하고 Queue 를 비운다.
- 더이상 참조되지 않으므로 메모리가 정리된다.
위처럼 GC 성능도 있고 필요한 경우도 적어서 대부분 Finalizer 를 정의하지 않는다. 그럼에도 정의되는 대표적인 경우는 IDisposable
을 상속받을 때 Finalizer 에서 Dispose()
를 호출하는 경우이다. 이때 Dispose()
함수에서 GC.SuppressFinalize(this)
같은 코드를 넣어 Finalizer Queue 에서 명시적으로 객체를 제거해 GC 과부화를 막는다. 이런 코드 패턴은 다음과 같다.
Disposable
Resource 관리는 두가지 특징이 있다.
- Resource 가 정리되는 시점을 사용자가 정할 수 있어야한다.
- 작업 중에 예외가 발생 시 Resource 정리가 반드시 수행되어야 한다.
위 특징을 고려하면 .Net 은 Memory 를 관리하지 Resource 를 관리하지 않음1을 이해할 수 있다. 예를들어 파일을 연다고 했을 때 파일을 닫는 것이 GC 에 의해 수행된다고 생각한다면 말이 안된다.
대신 Resource 관리를 위해 .Net 은 IDisposable
을 제공한다.
- 명시적으로
Dispose()
를 호출하여 정리 시점을 정할 수 있다. IDisposable
를 상속한 클래스는using
context 를 사용하면 중간에 예외가 발생해도 확실하게 객체를 파괴할 수 있다. 물론 기존 클래스를 변경하기 어렵고 단발적으로만 사용한다면try ... finally
을 쓸 수도 있지만 좋은 선택은 아니다.- 그리고 위처럼 Finalizer 에도 넣어 방어적으로 코드를 구성할 수도 있다.
unsafe
배열의 경계 체크, 네이티브 함수나 포인터를 가능하게 하지만 대부분은 사용하지 않는 기능이다.
- native dll 을 호출할 땐 필요하지만 P/Invoke 기능을 이용할 것이 권장된다.
- Managed Type 은 Value Type 에 대해서만 포인터 연산이 가능하다.
Fixed
fixed
는 다양하게 사용될 수 있다.
- gc 가 메모리 재배치를 못하게 막는
fixed
context 가 있다. - c# 에서 배열은 참조형이므로 구조체 내에 고정크기 버퍼를 만들 수 없다. 하지만
fixed int data[3]
처럼fixed
배열을 사용하면 가능하다.
C# 7.3 에서 GetPinnableReference()
로 관리 포인터(ref variable) 을 리턴하면 fixed
context 에서 자동으로 붙게 해줬다. 이는 Span<T>
외에 쓸일은 없다.
댓글남기기