# 가비지 수집기
가비지 수집기는 관리되는 메모리를 관장하며 네이티브 환경과는 다르게 메모리 누수, 댕글링 포인터, 초기화되지 않는 포인터, 여타의 메모리 관리 문제를 개발자들이 직접 다루지 않도록 자동화해줍니다.
개발자가 직접 메모리를 관리하는 경우에 발생할 수 있는 해제 순서의 선후 관계에 대한 문제나 순환 참조의 문제, 여러 객체들 간의 복잡한 연관 관계의 문제를 가비지 수집기는 알아서 처리해줍니다.
가비지 수집기는 COM처럼 개별 객체가 스스로 자신의 참조 여부나 횟수 등을 관리하도록 하지 않고, 응용프로그램의 최상위 객체로부터 개별 객체까지의 도달 가능 여부를 확인하도록 설계되어 있습니다. 응용프로그램 내의 최상위 객체로부터 참조 트리를 구성하여 도달 가능한 객체를 살아 있는 객체로 판단하고 도달 불가능한 객체를 가비지로 간주합니다.
# 리소스 관리에 대한 이해의 필요성
1. 가비지 수집기가 관리해주지 않는 비관리 리소스는 개발자가 직접 관리해야 합니다.
...더보기
관리 리소스 - CLR에 있는 자원들.
비관리 리소스 - 닷넷에 관리 자원이 아닌 자원. OS 자원.
(데이터베이스 연결, GDI+ 객체, COM 객체, 시스템 객체 등)
2. 이벤트 핸들러나 델리게이트 등도 잘못 사용하면 불필요하게 메모리에 오래 남게 됩니다.
때문에, 관리되는 리소스를 이해하고 올바른 해제 작업을 수행한다면 더욱 효과적인 가비지 수집이 가능해집니다.
# 비관리 리소스 해제
비관리 리소스의 생명주기에 대해 개발자가 더 손쉽게 관리할 수 있도록 finalizer와 IDisposable 인터페이스라는 두 가지 메커니즘을 제공합니다. 하지만, finalizer는 단점이 많기 때문에 IDisposable 인터페이스를 통해 리소스 관리할 것을 더욱 추천합니다.
- finalizer
비관리 리소스에 대한 해제 작업이 반드시 수행되도록 도와주는 방어적인 매커니즘입니다.
finalizer() 메서드는 직접 오버라이드하여 사용할 수 없고, 소멸자를 만듬으로써 오버라이드 됩니다. 소멸자가 만들어 지면 메모리 해제 작업이 필요하다는 것을 명시하게 됩니다. 개발자가 직접 관리하다보면 메모리 해제 작업을 까먹는 경우도 발생할 수 있는데, 이러한 경우에도 최소한 소멸자에서 호출할 수 있도록 하고 있습니다.
# finalizer가 성능에 미치는 영향
1. finalizer를 포함하고 있는 객체를 사용하면 가비지 수집 과정이 길어져, 비교적 오랫동안 관리되는 힙에 남아 있게 됩니다.
finalizer는 소멸자를 통해서 호출된다고 앞서 설명하였습니다.
아래와 같이 C++에서 대중적으로 사용되는 리소스 해제 구문이 C#에서는 동일한 방식으로 동작하지 않습니다.
class CriticalSection
{
public CriticalSection()
{
EnterCriticalSection();
}
//finalizer 오버라이드
~CriticalSection()
{
ExitCriticalSection();
}
}
가비지 수집기가 finalizer를 포함하는 객체를 가비지로 판단한 경우, 그 즉시 객체가 점유하고 있는 메모리 공간을 해제하지 못합니다. finalizer를 호출해야 하는데 가비지 수집을 수행하는 스레드를 통해 직접 finalizer를 호출할 수 없습니다.
때문에, 가비지 수집기는 이 객체에 대한 참조를 다른 큐에 삽입하여 나중에 finalizer가 호출될 수 있도록 사전 준비만을 수행합니다.
이 과정에서
- finalizer가 없는 객체는 메모리로부터 즉각 제거되지만,
- finalizer가 있는 객체는 이후 다시 가지비 수집 절차가 진행될 때 다른 큐에 삽입해 두었던 객체의 참조를 꺼내어 finalizer를 순차적으로 호출하여 메모리를 반납합니다.
이러한 이유로 finalizer를 포함한 객체들은 가비지 수집 과정이 더욱 길어집니다.
가비지 수집과정에 세대 개념에 따라 finalizer의 비용에 대해 생각해보면,
...더보기
[세대 개념]
0세대 : 가비지 수집 이후 임의의 생성된 객체.
1세대 : 다시 가비지 수집 절차가 수행되고 이후에도 살아있는 0세대 객체가 1세대 객체가 된다.
2세대 : 두 번 혹은 그 이상의 가비지 수집 절차가 수행됐음에도 여전히 사용되고 있는 객체.
[ 가비지 수집 과정 ]
가비지 수집 절차가 시작되면
1. 우선 0세대 객체에 대해서만 가비지 수집을 수행합니다.
2. 대략 10번에 한 번 꼴로 추가적으로 1세대 객체에 대해서 가비지 수집을 수행합니다.
3. 그리고 약 100번에 한번 꼴로 2세대 객체를 포함한 모든 세대의 객체를 대상으로 가비지 수집을 수행합니다.
finalizer를 가진 객체는 즉각 제거되지 못하므로 1세대 객체가 되고, 이 경우 9번의 가비지 수집 절차가 추가적으로 수행된 이후에 비로소 메모리에서 제거될 가능성이 있습니다. 만약 이 과정에서도 정리되지 못하고 2세대 객체가 되면 약 100번의 가비지 수집 절차가 추가로 수행된 후 삭제될 것입니다.
2. finalizer 메서드를 호출하는 스레드가 finalzier 스레드이기 때문에 특정 쓰레드에 선호도(affinity)를 갖는 객체일 경우 문제를 발생할 수 있다.
예를 들어, finalizer 메서드 내에서 잠금을 해제하려는 시도는 매우 위험합니다. 잠금을 잠근 스레드는 어플리케이션의 쓰레드이지만 잠금을 해제하려는 쓰레드는 finalizer 쓰레드이기 때문입니다.
3. finalizer 메서드가 호출되는 순서를 보장할 수 없습니다.
아래의 코드는 ObjectDisposedException 예외를 유발합니다.
class DangerousType
{
private StreamWriter _stream;
public DangerousType()
{
_stream = new StreamWriter("Test.txt");
}
~DangerousType()
{
_stream.Close(); // 오류가 발생할 수도 있다!
}
public void DoSomething()
{
//...... 생략 ......
}
}
DangerousType 객체는 멤버로 StreamWriter 객체를 참조하고 있고, StreamWriter 클래스 또한 finalzier 메서드를 정의하고 있다고 가정한다. 그러면 DangerousType 객체가 가비지 컬렉션의 대상이 되는 경우 두 객체가 모두 finalizer 큐에 삽입될 것입니다. 그리고 finalizer 쓰레드가 StreamWriter 객체에 대해 먼저 Finalizer 메서드를 호출한다면 StreamWriter 객체는 메모리를 반환하고, 그 이후 DangerousType의 Finalizer 메서드를 호출하면 이미 닫히 파일에 대해 또 다시 Close() 메서드가 호출되기 때문에 예외가 발생하게 됩니다.
리소스를 해제하는 가장 좋은 방법으로는, finalizer 사용을 지양하고 IDisposable 인터페이스와 표준 Dispose 패턴을 활용하는 것입니다.
해당 메모리 사용에 관한 내용은 이후 Item 12, Item 13, Item 14, Item 15, Item 16, Item 17에서 더 다루고 있습니다.
참 고 : http://www.simpleisbest.net/post/2011/05/12/Finalizer-Usage-Pattern.aspx