Szukaj…


Wprowadzenie

W .Net obiekty utworzone za pomocą new () są przydzielane do zarządzanej sterty. Te obiekty nigdy nie są jawnie finalizowane przez program, który ich używa; zamiast tego proces ten jest kontrolowany przez .Net Garbage Collector.

Niektóre z poniższych przykładów to „przypadki laboratoryjne” pokazujące Garbage Collectora w pracy i kilka istotnych szczegółów jego zachowania, podczas gdy inne skupiają się na tym, jak przygotować klasy do prawidłowej obsługi przez Garbage Collectora.

Uwagi

Garbage Collector ma na celu obniżenie kosztów programu pod względem przydzielonej pamięci, ale powoduje to koszty związane z czasem przetwarzania. Aby osiągnąć dobry ogólny kompromis, istnieje szereg optymalizacji, które należy wziąć pod uwagę podczas programowania z Garbage Collector:

  • Jeśli metoda Collect () ma być jawnie wywołana (co zresztą nie powinno tak często być), rozważ użycie trybu „zoptymalizowanego”, który finalizuje martwy obiekt tylko wtedy, gdy pamięć jest rzeczywiście potrzebna
  • Zamiast wywoływać metodę Collect (), rozważ użycie metod AddMemoryPressure () i RemoveMemoryPressure (), które wyzwalają kolekcję pamięci tylko wtedy, gdy jest to faktycznie potrzebne
  • Kolekcja pamięci nie gwarantuje sfinalizowania wszystkich martwych obiektów; zamiast tego Garbage Collector zarządza 3 „pokoleniami”, obiektami „czasami przeżywającymi” z pokolenia na następne
  • Może obowiązywać kilka modeli wątków, w zależności od różnych czynników, w tym dostrajania konfiguracji, co powoduje różne stopnie interferencji między wątkiem Garbage Collector a innymi wątkami

Podstawowy przykład zbierania (śmieci)

Biorąc pod uwagę następującą klasę:

public class FinalizableObject 
{
    public FinalizableObject()
    {
        Console.WriteLine("Instance initialized");
    }

    ~FinalizableObject()
    {
        Console.WriteLine("Instance finalized");
    }
}

Program, który tworzy instancję, nawet bez jej użycia:

new FinalizableObject(); // Object instantiated, ready to be used

Daje następujące dane wyjściowe:

<namespace>.FinalizableObject initialized

Jeśli nic innego się nie stanie, obiekt nie zostanie sfinalizowany, dopóki program się nie zakończy (co zwalnia wszystkie obiekty na zarządzanej stercie, finalizując je w trakcie procesu).

Można wymusić uruchomienie Garbage Collectora w danym punkcie w następujący sposób:

new FinalizableObject(); // Object instantiated, ready to be used
GC.Collect();

Co daje następujący wynik:

<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized

Tym razem, gdy tylko wywoływano Garbage Collector, nieużywany (aka „martwy”) obiekt został sfinalizowany i uwolniony z zarządzanej sterty.

Obiekty żywe i martwe - podstawy

Ogólna zasada: w przypadku wyrzucania elementów bezużytecznych obiekty „żywe” to te, które są nadal w użyciu, natomiast „martwe obiekty” to te, które nie są już używane (dowolna zmienna lub pole, które się do nich odwołują, wyłączyła się z zakresu przed gromadzeniem) .

W poniższym przykładzie (dla wygody FinalizableObject1 i FinalizableObject2 są podklasami FinalizableObject z powyższego przykładu i w ten sposób dziedziczą zachowanie komunikatu inicjalizacji / finalizacji):

var obj1 = new FinalizableObject1(); // Finalizable1 instance allocated here
var obj2 = new FinalizableObject2(); // Finalizable2 instance allocated here
obj1 = null; // No more references to the Finalizable1 instance 
GC.Collect();

Dane wyjściowe będą:

<namespace>.FinalizableObject1 initialized
<namespace>.FinalizableObject2 initialized
<namespace>.FinalizableObject1 finalized

W momencie wywołania Garbage Collector FinalizableObject1 jest martwym obiektem i zostaje sfinalizowany, a FinalizableObject2 jest obiektem aktywnym i jest przechowywany na zarządzanej stercie.

Wiele martwych obiektów

Co się stanie, jeśli dwa (lub kilka) martwych obiektów odniesie się do siebie? Jest to pokazane w poniższym przykładzie, zakładając, że OtherObject jest własnością publiczną FinalizableObject:

var obj1 = new FinalizableObject1(); 
var obj2 = new FinalizableObject2();
obj1.OtherObject = obj2;
obj2.OtherObject = obj1;
obj1 = null; // Program no longer references Finalizable1 instance
obj2 = null; // Program no longer references Finalizable2 instance
// But the two objects still reference each other
GC.Collect();

Daje to następujące dane wyjściowe:

<namespace>.FinalizedObject1 initialized
<namespace>.FinalizedObject2 initialized
<namespace>.FinalizedObject1 finalized
<namespace>.FinalizedObject2 finalized

Oba obiekty są finalizowane i uwalniane z zarządzanej sterty, mimo że się do nich odwołują (ponieważ nie istnieje żadne inne odniesienie do żadnego z nich z faktycznie aktywnego obiektu).

Słabe referencje

Słabe odniesienia to ... odniesienia do innych obiektów (zwanych też „celami”), ale „słabe”, ponieważ nie zapobiegają zbieraniu śmieci przez te obiekty. Innymi słowy, słabe referencje nie liczą się, gdy Garbage Collector ocenia obiekty jako „aktywne” lub „martwe”.

Następujący kod:

var weak = new WeakReference<FinalizableObject>(new FinalizableObject());
GC.Collect();

Daje wynik:

<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized

Obiekt jest zwalniany z zarządzanej sterty, mimo że odwołuje się do niego zmienna WeakReference (nadal w zasięgu, gdy wywołano moduł Garbage Collector).

Konsekwencja nr 1: w dowolnym momencie nie jest bezpieczne zakładanie, czy cel WeakReference jest nadal przydzielany do zarządzanej sterty, czy nie.

Konsekwencja nr 2: ilekroć program musi uzyskać dostęp do celu Słabej referencji, należy podać kod dla obu przypadków, dla celu wciąż przydzielonego lub nie. Metodą dostępu do celu jest TryGetTarget:

var target = new object(); // Any object will do as target
var weak = new WeakReference<object>(target); // Create weak reference
target = null; // Drop strong reference to the target

// ... Many things may happen in-between

// Check whether the target is still available
if(weak.TryGetTarget(out target))
{
    // Use re-initialized target variable
    // To do whatever the target is needed for
}
else
{
    // Do something when there is no more target object
    // The target variable value should not be used here
}

Ogólna wersja WeakReference jest dostępna od .Net 4.5. Wszystkie wersje frameworku udostępniają nietypową, nietypową wersję zbudowaną w ten sam sposób i sprawdzoną w następujący sposób:

var target = new object(); // Any object will do as target
var weak = new WeakReference(target); // Create weak reference
target = null; // Drop strong reference to the target

// ... Many things may happen in-between

// Check whether the target is still available
if (weak.IsAlive)
{
    target = weak.Target;

    // Use re-initialized target variable
    // To do whatever the target is needed for
}
else
{
    // Do something when there is no more target object
    // The target variable value should not be used here
}

Dispose () vs. finalizatory

Zaimplementuj metodę Dispose () (i zadeklaruj zawierającą klasę jako IDisposable) jako środek zapewniający zwolnienie zasobów o dużej pojemności pamięci, gdy tylko obiekt nie będzie już używany. „Złap” polega na tym, że nie ma silnej gwarancji, że metoda Dispose () zostanie kiedykolwiek wywołana (w przeciwieństwie do finalizatorów, które zawsze wywoływane są pod koniec życia obiektu).

Jednym ze scenariuszy jest program wywołujący Dispose () na obiektach, które jawnie tworzy:

private void SomeFunction()
{
    // Initialize an object that uses heavy external resources
    var disposableObject = new ClassThatImplementsIDisposable();

    // ... Use that object

    // Dispose as soon as no longer used
    disposableObject.Dispose();

    // ... Do other stuff 

    // The disposableObject variable gets out of scope here
    // The object will be finalized later on (no guarantee when)
    // But it no longer holds to the heavy external resource after it was disposed
}

Innym scenariuszem jest zadeklarowanie klasy, która ma zostać utworzona przez framework. W takim przypadku nowa klasa zazwyczaj dziedziczy klasę podstawową, na przykład w MVC tworzy się klasę kontrolera jako podklasę System.Web.Mvc.ControllerBase. Kiedy klasa podstawowa implementuje interfejs IDisposable, jest to dobra wskazówka, że Dispose () zostałby poprawnie wywołany przez framework - ale znowu nie ma silnej gwarancji.

Dlatego Dispose () nie zastępuje finalizatora; zamiast tego oba powinny być wykorzystywane do różnych celów:

  • Finalizator ostatecznie uwalnia zasoby, aby uniknąć wycieków pamięci, które mogłyby wystąpić w innym przypadku
  • Dispose () zwalnia zasoby (prawdopodobnie te same), gdy tylko nie są już potrzebne, aby zmniejszyć presję na ogólny przydział pamięci.

Właściwa utylizacja i finalizacja obiektów

Ponieważ Dispose () i finalizatory są przeznaczone do różnych celów, klasa zarządzająca zasobami obciążonymi pamięcią zewnętrzną powinna zaimplementować oba z nich. Konsekwencją jest napisanie klasy, aby dobrze obsługiwała dwa możliwe scenariusze:

  • Gdy wywoływany jest tylko finalizator
  • Kiedy najpierw wywoływana jest funkcja Dispose (), a później finalizator

Jednym z rozwiązań jest zapisanie kodu czyszczenia w taki sposób, że uruchomienie go raz lub dwa razy przyniosłoby taki sam wynik, jak uruchomienie go tylko raz. Wykonalność zależy od charakteru czyszczenia, na przykład:

  • Zamknięcie już zamkniętego połączenia z bazą danych prawdopodobnie nie przyniosłoby żadnego efektu, więc działa
  • Aktualizacja niektórych „liczników użycia” jest niebezpieczna i przy niewłaściwym wywołaniu wywołałaby niewłaściwy wynik.

Bezpieczniejszym rozwiązaniem jest z założenia zapewnienie, że kod czyszczenia jest wywoływany tylko raz i bez względu na kontekst zewnętrzny. Można to osiągnąć w „klasyczny sposób” za pomocą dedykowanej flagi:

public class DisposableFinalizable1: IDisposable
{
    private bool disposed = false;

    ~DisposableFinalizable1() { Cleanup(); }

    public void Dispose() { Cleanup(); }

    private void Cleanup()
    {
        if(!disposed)
        {
            // Actual code to release resources gets here, then
            disposed = true;
        }
    }
}

Alternatywnie Garbage Collector udostępnia określoną metodę SuppressFinalize (), która pozwala pominąć finalizator po wywołaniu Dispose:

public class DisposableFinalizable2 : IDisposable
{
    ~DisposableFinalizable2() { Cleanup(); }

    public void Dispose()
    {
        Cleanup();
        GC.SuppressFinalize(this);
    }

    private void Cleanup()
    {
        // Actual code to release resources gets here
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow