2012-10-20 16 views
6

Wystąpił problem z obiektami finalizowalnymi, które nie zostały zebrane przez GC, jeśli nie zostały jawnie wywołane. Wiem, że powinienem jawnie nazwać Dispose(), jeśli obiekt implementuje IDisposable, ale zawsze uważałem, że można bezpiecznie polegać na strukturze, a gdy obiekt staje się bez odniesienia, można go zebrać.Dlaczego obiekt nie jest zbierany z finalizerem, nawet jeśli nie jest unieważniony?

Ale po kilku eksperymentach z WinDbg/SOS/sosex Odkryłam, że jeśli GC.SuppressFinalize() nie został wezwany do finalizable obiektu nie zbiera się, nawet jeśli staje Nieukorzenione. Tak więc, jeśli intensywnie korzystasz z obiektów finalizowalnych (DbConnection, FileStream itp.) I nie wyrzucasz ich jawnie, możesz napotkać zbyt duże zużycie pamięci lub nawet OutOfMemoryException.

Oto przykładowa aplikacja:

public class MemoryTest 
{ 
    private HundredMegabyte hundred; 

    public void Run() 
    { 
     Console.WriteLine("ready to attach"); 
     for (var i = 0; i < 100; i++) 
     { 
      Console.WriteLine("iteration #{0}", i + 1); 
      hundred = new HundredMegabyte(); 
      Console.WriteLine("{0} object was initialized", hundred); 
      Console.ReadKey(); 
      //hundred.Dispose(); 
      hundred = null; 
     } 
    } 

    static void Main() 
    { 
     var test = new MemoryTest(); 
     test.Run(); 
    } 
} 

public class HundredMegabyte : IDisposable 
{ 
    private readonly Megabyte[] megabytes = new Megabyte[100]; 

    public HundredMegabyte() 
    { 
     for (var i = 0; i < megabytes.Length; i++) 
     { 
      megabytes[i] = new Megabyte(); 
     } 
    } 

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

    ~HundredMegabyte() 
    { 
     Dispose(false); 
    } 

    private void Dispose(bool disposing) 
    { 
    } 

    public override string ToString() 
    { 
     return String.Format("{0}MB", megabytes.Length); 
    } 
} 

public class Megabyte 
{ 
    private readonly Kilobyte[] kilobytes = new Kilobyte[1024]; 

    public Megabyte() 
    { 
     for (var i = 0; i < kilobytes.Length; i++) 
     { 
      kilobytes[i] = new Kilobyte(); 
     } 
    } 
} 

public class Kilobyte 
{ 
    private byte[] bytes = new byte[1024]; 
} 

Nawet po 10 powtórzeń może się okazać, że zużycie pamięci jest zbyt wysoka (od 700MB do 1GB) i staje się jeszcze większa z większą liczbą powtórzeń. Po dołączeniu do procesu za pomocą programu WinDBG można stwierdzić, że wszystkie duże obiekty są unieważnione, ale nie zostały zebrane.

Sytuacja zmienia się, jeśli jednoznacznie nazwiesz SuppressFinalize(): zużycie pamięci jest stabilne około 300-400 MB nawet pod wysokim ciśnieniem, a WinDBG pokazuje, że nie ma żadnych nie unieważnionych obiektów, pamięć jest darmowa.

Pytanie brzmi: czy jest to błąd w strukturze? Czy istnieje jakieś logiczne wytłumaczenie?

Więcej szczegółów:

Po każdej iteracji, windbg pokazuje, że:

  • kolejka zakończenie jest pusty
  • freachable kolejki jest pusty
  • generacja 2 zawiera obiekty (stu) z poprzednich iteracji
  • obiekty z poprzednich iteracji są wycofane z eksploatacji
+0

Próbowałem go uruchomić i dostałem go do 700mb, w którym to momencie obniżył się do 100mb lub tak na własną rękę. (Właśnie naciskałem return i obserwowałem użycie pamięci w Menedżerze zadań). System Windows 8. – keyboardP

+0

Bardzo interesujące ... testowałem go na win7 x64. Pierwsze 10 iteracji z jedną sekundową pauzą, po tym po prostu przytrzymaj dowolny klucz ... outofmemory – 6opuc

+0

Próbowałem ponownie, ale dostałem 100 iteracji bez wyjątku. Również 64-bitowy, ale całkiem interesujący. Może ktoś inny może również testować i publikować wyniki. – keyboardP

Odpowiedz

7

Obiekt z finalizatorem nie zachowuje się tak samo jak obiekt, który go nie ma.

Gdy pojawia się GC, a SuppressFinalize nie zostało wywołane, GC nie będzie w stanie odebrać instancji, ponieważ musi wykonać Finalizer. Dlatego finalizator jest wykonywany, ORAZ instancja obiektu jest promowana do generacji 1 (obiekt, który przetrwał pierwszy GC), nawet jeśli jest już bez żadnego odniesienia.

Obiekty generacji 1 (i Gen2) są uważane za długo żyjące i będą brane pod uwagę przy zbieraniu śmieci tylko wtedy, gdy GK Gen1 nie wystarczy, aby zwolnić wystarczającą ilość pamięci. Myślę, że podczas twojego testu Gen 1 GC jest zawsze wystarczający.

To zachowanie ma wpływ na wydajność GC, ponieważ neguje optymalizację, jaką daje kilka générations (masz obiekty krótkotrwałe w gen1).

Zasadniczo posiadanie Finalizera i niezapobieżenie wywoływaniu przez GC zawsze będzie promowało już martwe obiekty na długo żyjącej kupie, co nie jest dobrą rzeczą.

Dlatego należy odpowiednio utylizować swoje IDisposable obiektów i uniknąć finalizatory jeśli nie jest to konieczne

Edit (iw razie potrzeby wdrożyć IDisposable i nazywają GC.SuppressFinalize.): nie dobrze odczytać przykład kodu wystarczająco: Twoje dane wyglądają tak, jakby miały znajdować się w stosie dużych obiektów (LOH), ale tak naprawdę nie jest: Masz wiele małych tablic referencyjnych zawierających na końcu drzewa małe tablice bajtów.

Umieszczanie krótkiego obiektu w LOH jest jeszcze gorsze, ponieważ nie zostaną one skompaktowane ... I dlatego można uruchomić OutOfMemory z dużą ilością wolnej pamięci, jeśli CLR nie jest w stanie znaleźć pustego segmentu pamięci wystarczająco długo, aby zawierał dużą porcję danych.

+0

+1, ponieważ zgadzam się z każdym stwierdzeniem, ale chcę poczekać, jeśli ktoś napotka na te same problemy. Ta przykładowa aplikacja jest po prostu uproszczoną wersją naszego środowiska produkcyjnego, w której wydaje się, że niektóre używane frameworki (EF, spring.net, log4net) nie wywołują Dispose() dla strumieni i sqlcommands ... – 6opuc

+0

Sprawdziłem twoją sugestię około 2. generacji i odkrył, że nawet jeśli komentator jest komentowany, większość obiektów jest przydzielana (lub przenoszona) w drugim pokoleniu. Tak więc 2. gen zdecydowanie nie jest przyczyną tego problemu. – 6opuc

+0

Czy jesteś pewien, że uruchomiłeś GC? Środowisko CLR nie uruchomi go, dopóki nie skończy się pamięć. Spróbuj wywołać GC.Collect(), aby upewnić się, że jest uruchomiony. – Eilistraee

1

Myślę, że idea tego polega na tym, że implementując IDisposable, dzieje się tak dlatego, że zajmujesz się zasobami niezarządzanymi i musisz ręcznie pozbyć się zasobu.

Jeśli GC miał zadzwonić Dispose lub spróbować się go pozbyć, to wypróżniłby również niezarządzane rzeczy, które można bardzo dobrze wykorzystać gdzie indziej, GC nie ma sposobu, aby to wiedzieć.

Jeśli GC miał usunąć nierozwiązany obiekt, straciłbyś odniesienie do niezarządzanego zasobu, który prowadziłby do wycieków pamięci.

Więc ... Jesteś zarządzany, albo nie. Po prostu nie ma dobrego sposobu, aby GC poradziło sobie z niezadeklarowanymi IDisposables.

Powiązane problemy