Absolutne minimum o Dispose i GC pt. 1
Niniejszy artykuł ma przedstawić absolutne minimum na temat zachowania Garbage Collectora i kasowania nieużywanych obiektów z pamięci za pomocą metody Dispose(). Dlaczego to aż takie ważne? Wycieki pamięci są przyczyną wielu enigmatycznych błędów, których bez odpowiedniej wiedzy i narzędzi zwalczanie jest bardzo trudne.
Autor:Wojciech Turowicz
Źródło:http://wtbi.pl/blog/post/Absolutne-minimum-o-Dispose-i-GC.aspx/
Wielu już o tym pisało. Ja zamierzam zaprezentować informacje praktyczne, które można uzyskać przekopując przez dokumentację msdn. Zacznijmy od tego, że niewłaściwy kod wobec GC dotyczy zarówno zielonych, jak i tych bardziej doświadczonych programistów.
Najczęściej spotykane problemy, w których rozwiązaniu pomagałem:
* Wyjątek „OutOfMemory” zamyka aplikację losowo, nie wiadomo dlaczego.
* Wyjątek „Nie można utworzyć uchwytu okna”. Czy to bug Windowsa/.NET’a?
* Wyjątek „Za mało pamięci, aby kontynuować wykonywanie tego programu”
O co się tu rozchodzi? No więc nie jest to związane z pamięcią RAM ani dyskową.
Problem powoduje limit uchwytów dla procesu w systemie Microsoft Windows, który domyślnie wynosi 10,000. Ma to swoje uzasadnienie pod względem bezpieczeństwa systemu. Jeżeli nasz proces przekroczy limit to Windows zabija go natychmiastowo ponieważ broni się przed atakami.
Co to są te „Uchwyty”?
Uchwyt (ang. „Handle”) to nic innego jak wskaźnik do pewnego rodzaju obiektu. W .NET nie mamy do nich bezpośredniego dostępu, ale każda kontrolka jest zrobiona z kilku. Handle dzielą się na dwa rodzaje:
Skąd wiadomo ile proces „zużywa” handli? Odpowiedź jest prosta:
Skoro wiemy już aż tyle to przejdźmy do analizy powstawania problemu. Dlaczego tak się dzieje? Jeżeli już wiesz to prawdopodobnie nie musisz dalej czytać, ale może warto.
Do problemu dochodzi najczęściej przy podłączaniu EventHandlerów do Eventów. Jak się okazuje, nie można pisać „+=” zbyt frywolnie. W momencie kiedy podpinamy się pod jakieś zdarzenie obiekt emitujący je tworzy statyczną referencję do obiektu, który się zarejestrował. Graficznie wygląda to tak:
Po pewnym czasie obiekt B ma zostać usunięty (np. zamknięto okno z kontrolką, która miała metodę podłaczoną do zdarzenia jakiegoś innego obiektu, który nadal działa w pamięci). Garbage Collector weryfikuje czy obiekt B można wymazać i powstaje konflikt:
Efektem tego Obiekt B nie zostaje usunięty. Zostanie wyczyszczony dopiero kiedy obiekt A zostanie zakwalifikowany przez GC do wywalenia. Co gorsze, nie możemy już dokonać odpięcia obsługi zdarzenia w B, ponieważ straciliśmy go z tzw. „Application Scope” – zakresu dostępności obiektów. Powstaje „Memory Leak”, którego nie da się już wyleczyć. Kilkadziesiąt memory leaków potrafi przegiąć limit handli.
„Ale jak to? Przecież dałem Dispose()! Jaki ten .NET głupi! Wracam do pascala”. – w odpowiedzi na taką reakcję należy powiedzieć: „W tym szaleństwie jest metoda”. Garbage Collector ma być taką automatyczną skrzynią biegów. Biegi mają same wskakiwać i nie można wrzucić wstecznego kiedy jedziemy 200 km/h na autostradzie. Powyższa sytuacja to jest właśnie taka próba jazdy do tyłu.
Jakie jest rozwiązanie dla powyższego? Obiekt B ma odpiąć się od zdarzenia w momencie kiedy będzie już to niepotrzebne, np. w swoim własnym Dispose().
Podsumowując można powiedzieć, że metoda Dispose() tylko oznacza dla GC, że dany obiekt ma zostać przez niego usunięty. Nie oznacza to wcale, że tak się stanie. Można to sprawdzić w bardzo prosty sposób. Stwórz klasę która przeciża Dispose() i posiada własny ~Destruktor() . Doprowadź do powyższego konfliktu z inną klasą, ustaw breakpointy na początku dispose i początku destruktora i zobacz, w jakiej kolejności co się wykona.
Wniosek: Dispose oznacza co ma zostać usunięte (co nie oznacza, że tak się stanie), a destruktor jest wywoływany przez wątek GC w momencie „połykania” obiektu.
W następnym artykule skupię się na zwalczaniu już istniejących problemów związanych z zapchaniem pamięci.



(6 głosów, średnia: 3,83 / 5)






Zostaw odpowiedź