Sprzątanie śmieci nie zapobiega wyciekom

2011-08-26 20:14

Mogę się mylić, ale wydaje mi się, że potoczne wyobrażenia na temat odśmiecaczy pamięci (garbage collectiors) obejmują przekonanie, iż zapobiegają one każdemu problemowi właściwemu dla ręcznego zarządzaniu pamięcią. Jasne, wprowadzają przy tym swoje własne – jak choćby nieustalony czas życia obiektów – ale przynajmniej zapewniają nam, że nigdy nie “stracimy” zaalokowanego kawałka pamięci… Krótko mówiąc, garbage collectory podobno chronią nas całkowicie przed zjawiskiem wycieku pamięci.

Wiadomo, że nie jest to do końca prawdą. Wspominałem o tym chociażby w swoim artykule opisującym implementację w C++ odśmiecacza opartego o zliczanie referencji. Taki mechanizm nie potrafi posprzątać obiektów powiązanych zależnościami cyklicznymi (A \rightarrow B \rightarrow A). Jest tak dlatego, gdyż podbijają one wzajemnie swoje liczniki referencji i żaden z nich nie może w ten sposób osiągnąć zera. Nawet więc jeśli taki cykl nie jest dostępny z żadnego widocznego miejsca w programie, jego wewnętrzne odwołania będą utrzymywać go przy życiu.
To jednak dość znany przypadek, który występuje tylko w stosunkowo najprostszym mechanizmie odśmiecania. Bardziej wyrafinowane rozwiązania w rodzaju mark & sweep nie mają tej wady. Tam każda struktura odłączona od reszty programu będzie nieosiągalna dla algorytmu odśmiecacza i zostanie uprzątnięta. Z założenia eliminuje to więc wszystkie przypadki, które przy ręcznym zarządzaniu pamięcią kwalifikowałyby się jako jej wyciek.

Ale to jeszcze nie oznacza, że jakiekolwiek wycieki pamięci nie mają prawda się tu zdarzyć. Przeciwnie, są one jak najbardziej możliwe – i to ze zgoła przeciwnych powodów niż przy zarządzaniu wymagającym ręcznego zwalniania obiektów.

Przypomnijmy sobie najpierw, czym jest klasyczny wyciek pamięci (memory leak). W najprostszym przypadku wynika on z błędu w postaci prostego pominięcia free, delete czy release w jednym, oczywistym miejscu. Zazwyczaj jednak w grę wchodzi “zgubienie” wskaźnika, występujące przy przejściu przez którąś ze ścieżek wykonania programu, która pomija jego dealokację, gdy jest ona jeszcze możliwa. Problem polega więc na braku odwołań do obiektu, który wciąż jeszcze “żyje” i zajmuje miejsce w pamięci. Poprawnym postępowaniem przy ręcznym zarządzaniu pamięcią jest zatem utrzymywanie owych odwołań (wskaźników, referencji) dostatecznie długo: do momentu zwolnienia pamięci, na którą pokazują. W praktyce może to oczywiście nie być takie proste.

I tu pozornie pomaga garbage collector, bowiem zajmuje się tą kwestią za nas. Brak odwołań do istniejącego obiektu nie jest już problemem. Więcej: jest to sytuacja ze wszech miar pożądana. Im szybciej pozbędziemy się wszystkich odwołań do nieużywanej pamięci, tym szybciej zostanie ona poddana recyklingowi i będzie dostępna do ponownego wykorzystania. Nie mamy oczywiście gwarancji (nawet przy stosowaniu funkcji typu GC.Collect()), kiedy to się naprawdę stanie. Szybkie zerowanie wszystkich referencji do niepotrzebnych obiektów powinno jednak dawać statystyczny spadek ilości pamięci zajmowanej przez nasz program.
A co się stanie, jeśli tego nie zrobimy? I nie chodzi o przetrzymanie referencji odrobinę dłużej niż jest to rzeczywiście potrzebne. Co będzie, jeśli w ogóle o niej zapomnimy, pozwalając jej wciąż pokazywać na nieużywany już obiekt?… Ów obiekt nie będzie wówczas zwolniony przez odśmiecacz, ponieważ będzie temu zapobiegał nadmiar odwołań do niego prowadzących. Jego istnienie zostanie więc sztucznie podtrzymane, a pamięci, którą zajmuje, nie będzie już można wykorzystać do innych celów. Czy to nie brzmi znajomo?… Houston, mamy wyciek!

Powstawanie wycieków pamięci w językach programowania z GC polega zatem na istnieniu niepotrzebnych odwołań do nieużywanych obiektów – w przeciwieństwie do ich braku, które generowało wycieki przy ręcznym zarządzaniu. Być może nie jest wcale oczywiste, że takie niepotrzebne odwołania mogą w ogóle istnieć. Otóż jak najbardziej mogą; występują one wtedy, gdy jakiś długo żyjący obiekt (np. dostępny przez zmienne globalne/statyczne) “przywłaszcza” sobie inny (nazwijmy go X), zapamiętując referencję do niego. Gdy obiekt X powinien zostać zwolniony – bo cała reszta systemu poprawnie porzuciła wszystkie odwołania do niego – wciąż istnieje felerna referencja we wspomnianym długo żyjącym obiekcie globalnym.
W najgorszym wypadku referencja ta może nawet nie być widoczna w kodzie, a zawierający ją obiekt sam może być niedostępny! Jest to zupełnie możliwa sytuacja w Javie, oferującej mechanizm niestatycznych klas wewnętrznych:

  1. public class BigObject {
  2.     class Inner {
  3.         void foo() {  }
  4.     }
  5.     private static Inner inner = null;
  6.  
  7.     public BigObject() {
  8.         if (inner == null)
  9.             inner = new Inner();
  10.     }
  11. }

W tym kodzie statyczny (czyli globalny) obiekt klasy Inner będzie sztucznie utrzymywał przy życiu instancję BigObject nawet wtedy, gdy żadna inna referencja nie będzie już do niej prowadziła. Jako niestatyczny obiekt wewnętrzny posiada on bowiem referencję BigObject.this. Dodatkowo jest on zapamiętany w prywatnej składowej, więc nie ma do niego żadnego dostępu z zewnątrz. Oba obiekty są zatem stracone i stanowią wyciek pamięci.

Powyższy przykład jest naturalnie uproszczony i mimo że problematyczna referencja jest ukryta, to mikroskopijny zakres, który należy przejrzeć w jej poszukiwaniu czyni błąd stosunkowo łatwym do znalezienia. W skomplikowanym, rzeczywistym systemie nawet jawne odwołanie może natomiast przejść niezauważone – a właśnie do takich systemów garbage collector wydaje się najbardziej pasować.
Dlatego należy pamiętać, że automatyczne zarządzanie pamięcią może i jest automatyczne, ale wciąż jest zarządzaniem. GC może nam w nim jedynie pomóc, lecz nie zwalnia od myślenia o potencjalnych problemach.

Tags: ,
Author: Xion, posted under Programming »


7 comments for post “Sprzątanie śmieci nie zapobiega wyciekom”.
  1. brodny:
    August 26th, 2011 o 22:01

    Dziwne – napisałem komentarz, nie dodał. Próbowałem wrzucić ponownie – nie wrzucił, bo duplikat. Inny komentarz dodał… Oto, co chciałem powiedzieć :)

    O ile dobrze kojarzę coś, co mi się kiedyś przewinęło przed oczami podczas czytania jakiegoś artykułu (bodajże o wydajności aplikacji WPF, to chyba gdzieś tutaj było: http://msdn.microsoft.com/en-us/library/aa970683.aspx ), to podobną sytuację stanowią w .NET metody podpięte pod delegaty – obiekt niby zniszczony, nie istnieje, ale gdzieś sobie dynda odniesienie do jednej z jego metod, która obsługuje jakiekolwiek zdarzenie. Efekt może być niewidoczny, ale GC obiektu nie wyrzuci – skądinad słusznie.

  2. Sebas86:
    August 26th, 2011 o 22:06

    Hmmm, ciężko mi to nazwać wyciekiem pamięci, raczej bardzo specyficznym błędem logicznym, a i to zależy od tego czy autor tej klasy nie uczynił tego specjalnie – w tym przypadku sporo klas implementujących uproszczony wzorzec singleton powoduje wyciek pamięci.

    Chociaż czasami rzeczywiście zdarza się, że cieknie coś w ten sposób i oczywiście tego nie widać. Tutaj nasuwa mi się na myśl piękny babol, którego się dopuściłem ostatnio. Zapomniałem usuwać z głównego widoku, inny pełnoekranowy widok, po którym się rysowało. Oczywiście we wszystkich innych miejscach dealokacje były prawidłowe więc analiza statyczna oraz narzędzia do wykrywania wycieków niczego nie widziały, brakowało nieszczęsnego [myView removeFromSuperview].

  3. brodny:
    August 26th, 2011 o 22:12

    Moim zdaniem można to nazywać, jak komu wygodnie. Istotne jest, że problem istnieje. Dla mnie ten tekst był wypowiedzą w stylu “garbage collector jest dobrym narzędziem, ale jednak tylko narzędziem”. Nic nie zastąpi umiejętności myślenia użytkownika narzędzia – w tym przypadku programisty :)

  4. Xion:
    August 26th, 2011 o 22:34

    @brodny: Złapał cię filtr antyspamowy jak fałszywy pozytyw. Czasem mu się zdarza i jest to mała cena za skuteczną obronę przed 50-100 spamowymi komentarzami dziennie :)

    @Sebas86: ObjC podpada mi mimo wszystko pod zarządzanie ręczne, bo musisz sam dbać o właściwe domknięcie par retain/release. A twój błąd w języku GC raczej by nie wystąpił, o ile myView zostałoby sprzątnięte.

  5. Sebas86:
    August 27th, 2011 o 9:26

    @Xion: tak, jak najbardziej jest to ręczne zarządzanie pamięcią, jednak jest to tej samy klasy błąd/wyciek, o którym pisałeś. W Javie i każdym innym języku ze zaimplementowanym odśmiecaniem pamięci wyglądałby to zupełnie tak samo.

  6. Złowieszczy:
    September 11th, 2011 o 11:14

    Ten sam temat równie fajnie opisuje książka “Java efektywne programowania”, temat 6.

  7. Jarek Przygódzki:
    September 17th, 2011 o 14:12

    Nie jest to z pewnością powszechna praktyka, ale w przypadku aplikacji Javy warto czasami wykonać zrzut pamięci i przeanalizować go za pomocą Memory Analyzera (http://www.eclipse.org/mat/). Ja tak robię i uważam że warto – uzyskane w ten sposób informacje są po prostu bezcenne.

Comments are disabled.
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.