Garbage Collector podczas swojego cyklu wykonuje następujące czynności
- sprawdza czy dany obiekt jest przypisany do referencji
- defragmentuje zarządzaną stertę
- wolne miejsce scala do jednego bloku pamięci
Finalizery obniżają wydajność aplikacji. Gdy GC otrzyma zadanie usunięcia obiektu, który posiada finalizer nie zrobi tego od razu. Najpierw musi zostać wywołany finalizer. Wywołanie go nie odbywa się w tym samym wątku co działania GC. Obiekt taki trafia do kolejki obiektów, którym należy wywołać finalizera. GC nie przerywa w tym momencie swojej pracy. Przy następnym wywołaniu GC obiekty z wywołanym finalizerem zostają usunięte z pamięci. Wydawałoby się iż obiekty posiadające finalizer trwają w pamięci o jeden cykl więcej niż ich odpowiednicy bez niego. Tak nie jest. GC pracuje z tzw. generacjami. Jest ich 3: zerowa, pierwsza i druga. Każdy obiekt który przetrwał 1 cykl oczyszczania trafia do pierwszej generacji, obiekt który przetrwał 2 cykle lub więcej trafia do drugiej generacji.Tak więc:
- generacja 0 - przeważnie zmienne lokalne
- generacja 1 lub 2 - zmienne globalne
Rozwiązaniem jest poprawna implementacja wzorca IDisposable. Nic jednak nie pomoże tak GC, jak unikanie niepotrzebnego tworzenia obiektów. Przykładem złego tworzenia obiektów jest napisana poniżej funkcja OnPaint:
Code:
Metoda OnPaint wywoływana jest wielokrotnie podczas działania programu. Za każdym razem tworzony będzie obiekt czcionki (Font). W takim przypadku warto wyciągnąć tworzenie czcionki na zewnątrz funkcji:
Code:
Warto zauważyć tutaj także użycie klasy Brushes. Klasa ta może być wykorzystywana w dowolnym miejscu programu. Każdy kolor w tej klasie jest tworzony jako singleton. Nie jest więc tworzony odrębny obiekt pędzla a korzystamy z jednego dostępnego dla całego programu.
Kolejnym przykładem złego tworzenia obiektów jest łączenie długich stringów. Jeżeli łączymy wiele stringów należy stworzyć obiekt typu StringBuilder i za pomocą niego dokonać operacji na łańcuchu.
W przypadku hierarchii klas, klasa bazowa powinna implementować interfejs IDisposable. Dodatkowo implementujemy finalizer na przypadek gdy użytkownik zapomni zwolnić zasoby. Jeżeli klasa potomna także alokuje zasoby, które muszą zostać zwolnione, musi także implementować interfejs IDisposable oraz dodatkowo wywołać implementację z klasy bazowej.
Implementacja IDisposable musi spełnić 4 założenia:
- uwolnienie zasobów nie zarządzanych
- uwolnienie zasobów zarządzanych (np. uchwyty zdarzeń)
- ustawienie flagi mówiącej, że obiekt został zwolniony. W publicznych metodach powinno nastąpić sprawdzenie flagi i w przypadku jej ustawienia rzucenie wyjątku ObjectDisposedException
- Zapobiegnięcie finalizacji obiektu - wywołanie funkcji GC.SuppressFinalize(this)
W jaki sposób klasy potomne wyczyszczą swoje zasoby, a zarazem pozwolą klasie bazowej posprzątać po sobie? Jeżeli klasy dziedziczące nie wywołają czyszczenia z klasy bazowej zasoby nigdy nie zostaną zwolnione. Aby zwolnić te zasoby tworzymy wirtualną metodę Dispose, o następującej sygnaturze:
Code:
klasy potomne nadpisują tę metodę a na końcu wywołują wersję z klasy bazowej. W zależności od wartości parametru idDisposing:
- TRUE - czyścimy zarówno zasoby zarządzane jak i nie zarządzane
- FALSE - tylko zasoby nie zarządzane
Code:
Code:
Warto zauważyć, że flaga jest powielona - zarówno klasa bazowa jak i dziedzicząca ją posiada. Zapobiega to niepoprawnemu zwalnianiu zasobów tylko dla jednego typu.
Finalizer do klasy dodajemy tylko wtedy kiedy kod zawiera zasoby niezarządzane. W powyższym przypadku nie musiał zostać zdefiniowany, wręcz brak jego definicji wpłynie bardzo pozytywnie na wydajność.
Ważne jest także, aby operacja zwalniania nie wykonywała żadnych innych operacji niż tych do które jest przewidziana. Implementacja innych operacji niż tych które czyszczą pamięć, może dojść do niechcianego odtworzenia obiektu.
Brak komentarzy:
Prześlij komentarz