내용 보기

작성자

관리자 (IP : 172.17.0.1)

날짜

2022-03-22 09:20

제목

[C#] 다시 한번 기초 다지기! - GC의 Collect, WaitForFullGCComplete, finalize 메서드(소멸자)


Collect, WaitForFullGCComplete, finalize 메서드(소멸자)에 대해 설명하겠다.


위 포스팅에서 Collect와 WaitForFullGCComplete 메서드가 함께 사용된 부분을 발췌

// 가비지 컬렉터를 작동시킨다 System.GC.Collect(0, GCCollectionMode.Forced); System.GC.WaitForFullGCComplete();
GC.Collect() 메서드

역할
가비지 컬렉션을 (강제로) 수행하도록 함

형식

이름
설명
Collect()
모든
  세대의 가비지 수집을 즉시 수행합니다.
Collect(Int32)
0세대에서
  지정된 세대까지 가비지 수집을 즉시 수행합니다.
Collect(Int32, 

    GCCollectionMode)
GCCollectionMode 값에 지정된 시간에 0세대에서
  지정된 세대까지 가비지 수집을 수행합니다.
Collect(Int32, 

    GCCollectionMode, Boolean)
수집이
  차단되어야 할지 여부를 지정하는 값을 사용하여 GCCollectionMode 값으로 지정된 시간에 0세대에서 지정된 세대까지 가비지 수집을 강제로 실행합니다.
Collect(Int32, 

    GCCollectionMode, Boolean, Boolean)
수집이
  차단되고 압축되어야 할지 여부를 지정하는 값을 사용하여 GCCollectionMode 값으로 지정된 시간에 0세대에서 지정된 세대까지 가비지 수집을 강제로 실행합니다.

+ GCCollectionMode의 종류 - 언제 동작할건지

멤버 이름
설명
Default
이 열거형에 대한 기본 설정이며 현재는 Forced 입니다.
Forced
즉시 실행 되도록 가비지 수집을 수행합니다.
Optimized
가비지  수집기에게 지금이 객체 회수하기에 최적의 시간인지 결정하게끔 맡깁니다.


사용 예시

// 강제로 가비지 컬렉션을 작동 // 0세대에서 0세대까지만 가비지 수집을 즉시 실행한다. System.GC.Collect(0, GCCollectionMode.Forced); //System.GC.WaitForFullGCComplete();
//0세대부터 2세대까지 GC실행시키기에 최적의 시간인지 결정한 뒤 최적의 시간에 GC실행 System.GC.Collect(2, GCCollectionMode.Optimized);


Q. GC.Collect() 메서드를 자주 사용하는 게 좋을까?
A. 결론은 아니다.
그때 그때 가비지를 처리해주면 메모리 측면에서 좋을 거라고 생각하지만,
GC를 자주 돌리게 되면 참조 종료가 미쳐 마무리 되지 못한 채로 GC가 실행된다.
그러면 닷넷 런타임이 판단하여 자동으로 GC를 돌릴 때보다
0세대에 살아남은 객체들이 늘어나고, 그 객체들은 1세대로 넘어가게 된다.
이게 반복되면 세대가 점점 올라가 누적되는 객체들이 많아지고 자연스레 메모리 성능차원에서 좋지않게 된다.(단, 많은 메모리를 할당 받는 객체의 사용종료가 명확히예상되는 상황에서의 강제 수행은 이점얻을 수 있다.)


GC.WaitForFullGCComplete() 메서드

역할
공용 언어 런타임에 의한 전체 차단 가비지 컬렉션이 완료되었는지 여부를 확인하기 위해
등록된 알림의 상태를 반환함

형식

이름
설명
WaitForFullGCComplete()
공용
  언어 런타임에 의한 전체 차단 가비지 수집이 완료되었는지 여부를 확인하기 위한 등록된 알림의 상태를 반환합니다.
WaitForFullGCComplete(Int32)
공용
  언어 런타임에 의한 전체 차단 가비지 수집이 완료되었는지 여부를 확인하기 위한 등록된 알림의 상태를 지정된 제한 시간 내에 반환합니다.


구문
반환 타입이 System.GCNotificationStatus으로
GC.WaitForFullGCComplete();의 반환된 수행결과는 아래와 같다.
GCNotificationStatus.Succeeded -> 알림이 성공적으로 작성됐음
(+ CollectionCount메서드를 사용하여 수집 횟수를 기록할 수 있음)
GCNotificationStatus.Canceled -> 알림이 취소됐음

//WaitForFullGCComplete 메소드 정의 형식 public static GCNotificationStatus WaitForFullGCComplete()
// Check for a notification of a completed collection. GCNotificationStatus s = GC.WaitForFullGCComplete(); if (s == GCNotificationStatus.Succeeded) { Console.WriteLine("GC Notifiction raised."); OnFullGCCompleteEndNotify(); } else if (s == GCNotificationStatus.Canceled) { Console.WriteLine("GC Notification cancelled."); break; } else { // Could be a time out. Console.WriteLine("GC Notification not applicable."); break; }



GC의 소멸자 메서드
Finalize() - 소멸자

정의
GC에서 개체에 연결된 메모리를 해지(소멸)하기 전에 호출되는 메서드.
소멸자 비관리 자원(데이터연결 종료(DB 종료), 파일연결 stream, 네트워크 연결 소켓 등)과 윈도우 OS와 연동되는 핸들을 닫는 마무리 정리 역할을 한다.

형식
클래스와 동일한 이름 앞에 ~ 물결 기호를 붙인다.
Finalize()메서드를 명시적으로 구현하지 않는다. ~모양의 소멸자코드는 내부적으로 Finalize()코드로 변환된다.

class UnmanagedMemoryManager { ~UnmanagedMemoryManager() { /*관리되지 않는 리소스를 정리*/ } }

위의 소멸자코드는 아래의 코드로 대체된다

protected override void Finalize() { try { /*관리되지 않는 리소스를 정리*/ } finally { base.Finalize(); } }


특징
'예측불가'
소멸자가 호출되는 정확한 시점 또는 호출여부는 정의 되지 않는다.
GC가 언제 돌지 확실치 않으므로 중요한 동작은 소멸자 메서드에 구현하지 않는다.
그래도 GC가 동작한다면 소멸자는 호출되는 것이 보장된다.

단점
소멸자를 객체에 구현하면 현저한 성능 저하를 야기할 수 있다.
Why?
GC가 소멸자를 다루는 원리를 살펴보자.
① 객체에 소멸자를 구현하면 C# 컴파일러가 런타임시(new로 할당) 해당 객체 레퍼런스를
finalization Queue(종료 큐)에 집어 넣는다.



② GC가 한번 실행되면 소멸자가 없는 객체였다면 GC에 의해 바로 관리 힙에서
없어졌겠지만, 소멸자가 있기 때문에 종료 큐로부터 객체 레퍼런스를 꺼내
별도의 Freachable 큐에 또 다시 객체 레퍼런스를 보관해둔다.
-> 이 때 객체는 사라지지 않으므로 세대가 승격된다.



③ Freachable 큐에 객체 레퍼런스가 추가되면 해당 객체의 소멸자는 CLR에 의해 미리 생성해 둔 스레드가 호출해 준다.
이 스레드는 Freachabel 큐에 항목이 들어올 때마다 해당 객체를 꺼내서 소멸자를 실행하여 객체 레퍼런스를 제거한다. 따라서 대개의 경우 Freachable 큐는 다시 비어있는 상태가 된다.
-> 비로소 객체는 가비지화 된다.



④ 소멸자를 가졌던 객체는 소멸자를 가지지 않았던 일반 객체와 같은 상황으로 바뀐다.
이후 다시 한번 GC가 동작하면 객체는 관리 힙에서 제거된다.



위처럼 소멸자를 지닌 객체는
일반 객체라면 GC가 한 번 수행되는 걸로 끝나는 것과 달리
GC수행 - 소멸자 실행 - GC 수행 처럼 GC에게 더 많은 일을 시킨다.
그리고 세대가 승격화 됨에 따라 메모리에 더 오래 남아있게 된다.
-> 따라서 긴 시간이 소요될 수 있고, GC에 부담을 주기 때문에
특별한 이유가 없다면 소멸자를 추가하지 않는 것을 권장한다.


**이렇게 불편한데 언제 사용할까?
GC의 관리 범위를 벗어나는 "비관리 자원"(Managed Heap이 아닌 곳)해제시 필요하다.
비관리 자원(네트워크, 파일연결 stream, DB커넥션) 또는 윈도우 운영체제와 연동되는 핸들과 같은 자원은 GC의 관리 범위를 벗어나므로 개발자가 직접 해제를 담당해야 한다.
(해제하지 않을 경우 OutOfMemoryException을 보게 될 것..!ㅎㄷㄷ)

아래 테스트를 통해 비관리 메모리 자원을 할당하는 메서드를 살펴보자.







using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace Destructor_sample { class Program { static void Main(string[] args) { while(true) { UnmanagedMemoryManager m = new UnmanagedMemoryManager(); m = null; GC.Collect(); // GC를 강제로 수행 //현재 프로세스가 사용하는 메모리 크기 출력 Console.WriteLine(Process.GetCurrentProcess().PrivateMemorySize64); } } } class UnmanagedMemoryManager { IntPtr pBuffer; public UnmanagedMemoryManager() { //AllocCoTaskMem메서드는 비관리 메모리를 할당한다. pBuffer = Marshal.AllocCoTaskMem(4096 * 1024);//의도적으로 4MB 할당 } } }

결과: OutOfMemoryException 에러 발생








설명: UnmanagedMemoryManager 클래스는 생성자에서 4MB 크기의 비관리 메모리를 할당한다.
이렇게 할당된 메모리는 GC의 관리 힙에 위치하지 않기 때문에 Main 메서드의 while 무한 루프에서
강제로 GC.Collect를 호출하더라도 수거되지 않는다.
따라서 위의 프로그램을 실행하면 얼마 지나지 않아 32비트 프로세스의 사용 가능한 2GB 메모리 용량을 모두 소진해 OutOfMemoryException 예외가 발생된다.
-> 따라서 비관리 메모리를 사용하는 프로그램은 반드시 해당 자원을 해제하는 코드도 클래스에 구현해야만 한다. AllocCoTaskMem으로 할당한 메모리는 반드시 개발자가 FreeCoTaskMem 메서드를 통해 해제해야 한다.
아래 예제는 명시적으로 Dispose메서드를 호출해 준 경우이다.
Dispose메서드로 메모리가 잘 해제되므로 더 이상 OutOfMemoryException 예외는 발생하지 않는다.

using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace Destructor_sample { class Program { static void Main(string[] args) { while(true) { //Dispose()메소드를 자동 실행 using (UnmanagedMemoryManager m = new UnmanagedMemoryManager()) { } //현재 프로세스가 사용하는 메모리 크기 출력 Console.WriteLine(Process.GetCurrentProcess().PrivateMemorySize64); } } } class UnmanagedMemoryManager : IDisposable { IntPtr pBuffer; public UnmanagedMemoryManager() { //AllocCoTaskMem메서드는 비관리 메모리를 할당한다. pBuffer = Marshal.AllocCoTaskMem(4096 * 1024);//의도적으로 4MB 할당 } //Dispose()메소드 구현 public void Dispose() { Marshal.FreeCoTaskMem(pBuffer); } } }


아래 예제는 Dispose를 명시적으로 호출하지 않았지만, 클래스에 포함된 소멸자 덕분에
OutOfMemoryException 예외(메모리 부족현상)가 발생하지 않는다.
(GC.Collect()메서드를 주석처리해도 결과는 같다.)

using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace Destructor_sample { class Program { static void Main(string[] args) { while(true) { //Dispose()메소드를 자동 실행 UnmanagedMemoryManager m = new UnmanagedMemoryManager(); m = null; //GC로 인해 소멸자가 호출되므로 비관리 메모리도 해제됨. //GC.Collect()를 명시적으로 부르지 않아도 메모리가 부족하면 //GC는 자동적으로 실행됨 GC.Collect(); //현재 프로세스가 사용하는 메모리 크기 출력 Console.WriteLine(Process.GetCurrentProcess().PrivateMemorySize64); } } } class UnmanagedMemoryManager : IDisposable { IntPtr pBuffer; bool _disposed; public UnmanagedMemoryManager() { //AllocCoTaskMem메서드는 비관리 메모리를 할당한다. pBuffer = Marshal.AllocCoTaskMem(4096 * 1024);//의도적으로 4MB 할당 } //Dispose()메소드 구현 public void Dispose() { if (_disposed == false) { Marshal.FreeCoTaskMem(pBuffer); _disposed = true; } } ~UnmanagedMemoryManager()//소멸자: 가비지 수집이 되면 호출된다. { Dispose(); } } }

소멸자는 GC가 동작한다면 호출되는 것이 보장된다.
위의 예제처럼 ~UnmanagedMemoryManager로 정의된 소멸자는 가비지 수집이 발생하면 호출되는 기회를 얻는다. 이 때문에 개발자가 Dispose 메서드의 호출 코드를 잊어버렸더라도 GC가 발생할 때까지 시간은 좀 걸리겠지만 소멸자에 정의된 자원 해제 코드가 언젠가 실행되므로 메모리 누수 현상이 사라진다. 즉, 클래스를 만든 개발자가 해당 클래스를 사용하는 개발자의 실수를 예상하고 방어적인 차원에서 자원 해제 코드를 넣어 두는 곳이 소멸자다.

(Dispose()만 정의해뒀는데 만약에 까먹고 안부르면 에러 뜨니까.. 안심차원에서 소멸자를 정의한 것!)

그런데 아까 위의 그림에서 설명했듯이, 소멸자를 사용하면 GC는 더 많은 일을 하게된다는 단점이 있다. 개발자가 까먹지 않고 명시적으로 Dispose 메서드를 명시적으로 호출해 줬다면 굳이 소멸자가 호출될 필요가 없다.
-> 즉, Dispose가 호출된 객체는 GC가 그 객체를 관리 힙에서 제거하는 과정에서
종료 큐에 대한 고려를 하지 않아도 된다. (GC는 일이 훨씬 줄어든다!!)
-> 그래서 MS에서는 이처럼 명시적인 자원 해제가 됐을 경우 종료 큐에서 객체를 제거하는 GC.SuppressFinalize 메서드를 제공한다.

아래 예제는 GC.SuppressFinalize 메서드를 이용해 Dispose와 소멸자를 다음과 같이 재정의했다.

using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace Destructor_sample { class Program { static void Main(string[] args) { while(true) { //Dispose()메소드를 자동 실행 using (UnmanagedMemoryManager m = new UnmanagedMemoryManager()) { } //GC.Collect();//GC로 인해 소멸자가 호출되므로 비관리 메모리도 해제됨. //현재 프로세스가 사용하는 메모리 크기 출력 Console.WriteLine(Process.GetCurrentProcess().PrivateMemorySize64); } } } class UnmanagedMemoryManager : IDisposable { IntPtr pBuffer; bool _disposed; public UnmanagedMemoryManager() { //AllocCoTaskMem메서드는 비관리 메모리를 할당한다. pBuffer = Marshal.AllocCoTaskMem(4096 * 1024);//의도적으로 4MB 할당 } //Dispose()메소드 구현 public void Dispose(bool disposing) { //Dispose()메소드를 명시적으로 호출하든 //소멸자를 통해 호출하든 공통적으로 동작 if (_disposed == false) { Marshal.FreeCoTaskMem(pBuffer); _disposed = true; } //Dispose()메소드를 명시적으로 호출한 경우 if(disposing == false) { //disposing이 false인 경우란 명시적으로 Dispose()를 호출한 경우다. //따라서 종료 큐에서 자신을 제거해 GC의 부담을 줄인다. GC.SuppressFinalize(this); } } public void Dispose() { Dispose(false); } ~UnmanagedMemoryManager()//소멸자: 가비지 수집이 되면 호출된다. { Dispose(true); } } }

위의 예제에서 new로 객체를 생성하면 종료 큐에 객체가 추가된다.
하지만 using{}구문으로 Dispose메서드를 자동으로 호출했기때문에 GC.SuppressFinalize메서드가 실행됨으로써 종료 큐에서 객체가 제거된다.
-> 따라서 소멸자가 정의되지 않은 객체와 동일한 상태로 바뀌기 때문에 결과적으로는 GC의 부담을 덜어준다.
-> 위의 예제는 재사용이 가능한 패턴이므로 필요할 때 사용하면 된다.
나중에 비관리 자원을 해제해야 할 상황이 생긴다면 위의 코드에서
Marshal, FreeCoTaskMem 메서드를 호출하는 부분만 교체해서 사용하면 된다.


출처1

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sam_sist&logNo=221002701762

출처2