IT

.NET의 가비지 수집 이해

lottoking 2020. 6. 4. 08:05
반응형

.NET의 가비지 수집 이해


아래 코드를 고려하십시오.

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

이제 주 메소드의 변수 c1이 범위를 벗어 났고 GC.Collect()호출 될 때 다른 객체에서 더 이상 참조하지 않더라도 왜 종료되지 않습니까?


디버거를 사용하고 있기 때문에 여기에 걸려 넘어져서 매우 잘못된 결론을 도출하고 있습니다. 사용자 컴퓨터에서 코드가 실행되는 방식으로 코드를 실행해야합니다. 빌드 + 구성 관리자를 사용하여 릴리스 빌드로 먼저 전환하고 왼쪽 상단 모서리에있는 "액티브 솔루션 구성"콤보를 "릴리스"로 변경하십시오. 다음으로 Tools + Options, Debugging, General로 가서 "Suppress JIT 최적화"옵션을 해제하십시오.

이제 프로그램을 다시 실행하고 소스 코드를 수정하십시오. 여분의 괄호가 전혀 효과가 없는지 확인하십시오. 변수를 null로 설정해도 아무런 차이가 없습니다. 항상 "1"을 인쇄합니다. 이제 원하는대로 작동하고 작동 할 것으로 예상했습니다.

디버그 빌드를 실행할 때 왜 다르게 작동하는지 설명하는 작업이 남습니다. 가비지 콜렉터가 로컬 변수를 발견하는 방법과 디버거를 제공함으로써 그 영향을받는 방법을 설명해야합니다.

우선, 지터 는 메소드의 IL을 기계 코드로 컴파일 할 때 두 가지 중요한 임무를 수행 합니다. 첫 번째는 디버거에서 매우 잘 보입니다. Debug + Windows + Disassembly 창에서 머신 코드를 볼 수 있습니다. 그러나 두 번째 의무는 완전히 보이지 않습니다. 또한 메소드 본문 내의 로컬 변수가 사용되는 방법을 설명하는 테이블을 생성합니다. 이 테이블에는 각 메소드 인수와 두 개의 주소가있는 로컬 변수에 대한 항목이 있습니다. 변수가 객체 참조를 처음 저장할 주소입니다. 그리고 해당 변수가 더 이상 사용되지 않는 머신 코드 명령어의 주소입니다. 또한 해당 변수가 스택 프레임 또는 CPU 레지스터에 저장되어 있는지 여부입니다.

이 테이블은 가비지 콜렉터에 필수적이며 콜렉션을 수행 할 때 오브젝트 참조를 찾을 위치를 알아야합니다. 참조가 GC 힙에있는 객체의 일부인 경우 매우 쉽습니다. 객체 참조가 CPU 레지스터에 저장 될 때 확실히 쉽지 않습니다. 표는 어디를 봐야하는지 알려줍니다.

테이블에서 "더 이상 사용되지 않는"주소는 매우 중요합니다. 가비지 수집기를 매우 효율적으로 만듭니다. 메소드 내에서 사용되고 해당 메소드가 아직 실행을 완료하지 않은 경우에도 오브젝트 참조를 수집 할 수 있습니다. 예를 들어 Main () 메서드는 프로그램이 종료되기 직전에 실행을 중지합니다. 분명히 Main () 메서드 내에서 사용되는 객체 참조가 프로그램 지속 시간 동안 유지되는 것을 원하지 않을 것입니다. 지터는 테이블을 사용하여 호출하기 전에 Main () 메서드 내에서 프로그램이 진행된 정도에 따라 이러한 로컬 변수가 더 이상 유용하지 않다는 것을 알 수 있습니다.

해당 테이블과 관련된 거의 마술적인 방법은 GC.KeepAlive ()입니다. 그것은이다 매우 그것은 전혀 코드를 생성하지 않으며, 특별한 방법. 유일한 의무는 해당 테이블을 수정하는 것입니다. 그것은 확장로컬 변수의 수명으로 인해 저장된 참조에서 가비지가 수집되지 않습니다. 참조를 수집 할 때 GC가 열망하는 것을 막아야 할 때만 참조가 관리되지 않는 코드로 전달되는 interop 시나리오에서 발생할 수 있습니다. 가비지 콜렉터는 지터에 의해 컴파일되지 않았으므로 해당 코드에서 사용되는 이러한 참조를 볼 수 없으므로 참조를 찾을 위치를 나타내는 테이블이 없습니다. EnumWindows ()와 같이 관리되지 않는 함수에 대리자 개체를 전달하는 것이 GC.KeepAlive ()를 사용해야하는 경우의 대표적인 예입니다.

So, as you can tell from your sample snippet after running it in the Release build, local variables can get collected early, before the method finished executing. Even more powerfully, an object can get collected while one of its methods runs if that method no longer refers to this. There is a problem with that, it is very awkward to debug such a method. Since you may well put the variable in the Watch window or inspect it. And it would disappear while you are debugging if a GC occurs. That would be very unpleasant, so the jitter is aware of there being a debugger attached. It then modifies the table and alters the "last used" address. And changes it from its normal value to the address of the last instruction in the method. Which keeps the variable alive as long as the method hasn't returned. Which allows you to keep watching it until the method returns.

This now also explains what you saw earlier and why you asked the question. It prints "0" because the GC.Collect call cannot collect the reference. The table says that the variable is in use past the GC.Collect() call, all the way up to the end of the method. Forced to say so by having the debugger attached and by running the Debug build.

Setting the variable to null does have an effect now because the GC will inspect the variable and will no longer see a reference. But make sure you don't fall in the trap that many C# programmers have fallen into, actually writing that code was pointless. It makes no difference whatsoever whether or not that statement is present when you run the code in the Release build. In fact, the jitter optimizer will remove that statement since it has no effect whatsoever. So be sure to not write code like that, even though it seemed to have an effect.


One final note about this topic, this is what gets programmers in trouble that write small programs to do something with an Office app. The debugger usually gets them on the Wrong Path, they want the Office program to exit on demand. The appropriate way to do that is by calling GC.Collect(). But they'll discover that it doesn't work when they debug their app, leading them into never-never land by calling Marshal.ReleaseComObject(). Manual memory management, it rarely works properly because they'll easily overlook an invisible interface reference. GC.Collect() actually works, just not when you debug the app.


[ Just wanted to add further on the Internals of Finalization process ]

So, you create an object and when the object is collected, the object's Finalize method should be called. But there is more to finalization than this very simple assumption.

SHORT CONCEPTS::

  1. Objects NOT implementing Finalize methods, there Memory is reclaimed immediately,unless of course, they are not reacheable by
    application code anymore

  2. Objects implementing Finalize Method, The Concept/Implementation of Application Roots, Finalization Queue, Freacheable Queue comes before they can be reclaimed.

  3. Any object is considered garbage if it is NOT reacheable by Application Code

Assume:: Classes/Objects A, B, D, G, H do NOT implement Finalize Method and C, E, F, I, J implement Finalize Method.

When an application creates a new object, the new operator allocates the memory from the heap. If the object's type contains a Finalize method, then a pointer to the object is placed on the finalization queue.

therefore pointers to objects C, E, F, I, J gets added to finalization queue.

The finalization queue is an internal data structure controlled by the garbage collector. Each entry in the queue points to an object that should have its Finalize method called before the object's memory can be reclaimed. Figure below shows a heap containing several objects. Some of these objects are reachable from the application's roots, and some are not. When objects C, E, F, I, and J were created, the .Net framework detects that these objects have Finalize methods and pointers to these objects are added to the finalization queue.

enter image description here

When a GC occurs(1st Collection), objects B, E, G, H, I, and J are determined to be garbage. Because A,C,D,F are still reacheable by Application Code depicted through arrows from yellow Box above.

The garbage collector scans the finalization queue looking for pointers to these objects. When a pointer is found, the pointer is removed from the finalization queue and appended to the freachable queue ("F-reachable").

The freachable queue is another internal data structure controlled by the garbage collector. Each pointer in the freachable queue identifies an object that is ready to have its Finalize method called.

After the collection(1st Collection), the managed heap looks something similar to figure below. Explanation given below::
1.) The memory occupied by objects B, G, and H has been reclaimed immediately because these objects did not have a finalize method that needed to be called.

2.) However, the memory occupied by objects E, I, and J could not be reclaimed because their Finalize method has not been called yet. Calling the Finalize method is done by freacheable queue.

3.) A,C,D,F are still reacheable by Application Code depicted through arrows from yellow Box above, So they will NOT be collected in any case

enter image description here

There is a special runtime thread dedicated to calling Finalize methods. When the freachable queue is empty (which is usually the case), this thread sleeps. But when entries appear, this thread wakes, removes each entry from the queue, and calls each object's Finalize method. The garbage collector compacts the reclaimable memory and the special runtime thread empties the freachable queue, executing each object's Finalize method. So here finally is when your Finalize method gets executed

The next time the garbage collector is invoked(2nd Collection), it sees that the finalized objects are truly garbage, since the application's roots don't point to it and the freachable queue no longer points to it(it's EMPTY too), Therefore the memory for the objects (E, I, J) are simply reclaimed from Heap.See figure below and compare it with figure just above

enter image description here

The important thing to understand here is that two GCs are required to reclaim memory used by objects that require finalization. In reality, more than two collections cab be even required since these objects may get promoted to an older generation

NOTE:: The freachable queue is considered to be a root just like global and static variables are roots. Therefore, if an object is on the freachable queue, then the object is reachable and is not garbage.

As a last note, remember that debugging application is one thing, Garbage Collection is another thing and works differently. So far you can't FEEL garbage collection just by debugging applications, further if you wish to investigate Memory get started here.


There are 3 ways in which you can implement memory management:-

GC works only for managed resources, therefore .NET provide Dispose and Finalize to release unmanaged resources like stream, database connection, COM objects etc..

1) Dispose

Dispose must be called explicitly for types which implements IDisposable.

Programmer must call this either using Dispose() or via Using construct

Use GC.SuppressFinalize(this) to prevent call to Finalizer if you have already used dispose()

2) Finalize or Distructor

It is called implicitly after object is eligible for cleanup, finalizer for objects are called sequentially by finalizer thread.

Drawback of implementing finalizer is that it memory reclaim gets delayed as finalizer for such class/types must be called prior cleanup, so an additional colect to reclaim memory.

3) GC.Collect()

Using GC.Collect() doesn't necessarily put GC for collection, GC can still override and run whenever it wants to.

also GC.Collect() will only run the tracing portion of garbage collection and add items to finalizer queue but not call finalizers for the types, that is handled by another thread.

Use WaitForPendingFinalizers if you want to make sure all finalizers have been callled after you invoke GC.Collect()

참고URL : https://stackoverflow.com/questions/17130382/understanding-garbage-collection-in-net

반응형