About Java referencesThere is somewhat common misconception about garbage collecting, that it totally frees the programmer from memory-related concerns. Granted, it makes the task easier in great many cases, but it does so at the expense of significant loss of control over objects’ lifetime. Normally, they are kept around for at least until they are not needed anymore – and usually that’s fine for the typical definitions of “need” and “at least”. Usually – but not always.
For those less typical use cases, garbage-collected environments provide mechanisms allowing to regain some of that lost control, to the extent necessary for particular task. Java, for example, offers a variety of different types of references, enabling to change the notion of what it means for an object to be eligible for garbage collecting. Choosing the right one for a problem at hand can be crucial, especially if we are concerned with the memory footprint of our application. Since – as the proverb goes – JVM expands to fill all available memory, it’s good to know about techniques which help maintain our heap size in check.
So today, I will discuss the SoftReference and WeakReference classes, which can be both found in the java.lang.ref package. They provide the so-called soft and weak references, which are both considerably less powerful when it comes to prolonging the lifetime of an object.
Sprzątanie śmieci nie zapobiega wyciekomMogę 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 (
). 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.
using w C#W C++ nie ma mechanizmu typu garbage collector, więc jedyne automatyczne zwalnianie obiektów, jakie w tym języku występuje, dotyczy tych lokalnych - tworzonych na stosie. Dlatego wszelkiego typu pomocnicze obiekty (np. uchwyty do zewnętrznych zasobów, jak pliki) deklaruje się tu zwykle jako właśnie zmienne lokalne.
W innych językach z kolei - dokładniej: w tych, w których GC występuje - praktycznie wszystkie obiekty są tworzone na stercie i zarządzane przez odśmiecacz pamięci. Nie musimy więc martwić się o to, jak i kiedy zostaną one zwolnione.
Ta zaleta staje się jednak wadą w sytuacji, gdy chcielibyśmy jednak móc swoje obiekty niszczyć samodzielnie. Jeśli na przykład mamy do czynienia ze wspominanym już uchwytem do zewnętrznego zasobu, to pewnie życzylibyśmy sobie, by został on zamknięty jednak nieco wcześniej niż na końcu działania programu (w niezbyt dużych aplikacjach zazwyczaj dopiero wtedy włącza się garbage collector). Inaczej będziemy niepotrzebnie zjadać zasoby systemowe.
W C# najlepszym sposobem na ograniczenie czasu życia obiektu jest instrukcja using (w tym kontekście to słowo kluczowe nie znaczy wcale użycia przestrzeni nazw!). Podajemy jej po prostu obiekt, którego chcemy użyć wewnątrz bloku; w zamian mamy zapewnione, że związane z nim zasoby zostaną zwolnione po wyjściu z tego bloku. Prosty przykład wygląda choćby tak:
Czemu jednak samodzielnie nie wywołać tego Close czy innej podobnej metody, która służy do zwolnienia zasobu?... Ano choćby dlatego, że istnieje coś takiego jak wyjątki. O ile powoływanie się na ten fakt w C++ bywa zwykle nadmiarem ostrożności, o tyle w .NET wyjątki latają praktycznie stale i mogą być rzucane przez właściwie każdą instrukcję. Nie można więc pomijać możliwości ich wystąpienia i liczyć na to, że mimo niedbałego kodowania wyciek zasobów jakimś cudem nigdy nam się nie trafi.
Może więc lepiej użyć zwykłego bloku try-finally? Zasadniczo using jest mu równoważny, a ponadto ma jeszcze dodatkowe zalety: automatycznie sprawdza istnienie obiektu przez jego zwolnieniem i ogranicza zasięg zmiennej przechowującej do niego referencję (jeśli deklarujemy ją tak, jak powyżej). Ponadto pozwala też nie wnikać w to, jaką metodę - Close, Disconnect, Release, End, ... - trzeba by wywołać na koniec w bloku finally. Jako że wymagane jest, by obiekt w using implementował interfejs IDisposable, będzie to zawsze metoda Dispose, która zawsze posprząta i pozamyka wszystko co trzeba.
Sprzątanie śmieciPamięcią operacyjną można w programowaniu zarządzać na dwa sposoby. Pierwszy to ręczne tworzenie obiektów i niszczenie ich, gdy nie są już potrzebne. Daje to kontrolę nad czasem ich życia, ale dopuszcza też możliwość powstawania błędów, jak wycieki pamięci czy próby podwójnego jej zwalniania. Aby im zapobiec, każdy obiekt musi mieć ściśle określonego właściciela, odpowiedzialnego za jego zniszczenie.
Drugi sposób to użycie mechanizmu odśmiecania pamięci (garbage collecting), które powinien sam wykrywać "porzucone" obiekty i je zwalniać, kiedy zachodzi ku temu potrzeba. Pozwala to oczywiście przestać martwić się o ich niszczenie. Zwykle nie oznacza to jednak, że wszystkie wyciekające fragmenty pamięci zostaną zwolnione natychmiast. Tracimy więc kontrolę nad czasem życia obiektów.
Nie da się jednak ukryć, że od kiedy komputery mają dość mocy obliczeniowej, aby wyświetlać miękkie cienie pod okienkami, mogą też z powodzeniem zajmować się automatycznym porządkowaniem sterty w swoim wolnym czasie. Dlatego zdecydowana większość nowych języków programowania jest wyposażona w odśmiecacze, które na dodatek są zawsze włączone i zwykle nie da się z nich zrezygnować. Najlepiej byłoby naturalnie mieć tutaj wybór, lecz rzadko jest on nam dany.
Nie inaczej jest w C++, tyle że tutaj mamy chyba jednak tę gorszą opcję - czyli konieczność ręcznego zarządzania alokacją i zwalnianiem. Można aczkolwiek to zmienić, lecz nie odbędzie się to w sposób przezroczysty dla programisty.
Odśmiecanie można przeprowadzić dwiema podstawowymi metodami, które mają naturalnie wiele wariantów. Są to:
W swoim pierwszym (działającym :)) ośmiecaczu dla C++ zastosowałem drugą metodę - oczywiście ze względu na jej prostotę. Jak wiadomo jednak nie jest ona doskonała, gdyż jej piętą achillesową są odwołania cykliczne. Można jej zaradzić na przykład poprzez tak zwane słabe referencje... Ale na szczęście póki co nie potrzebuję jeszcze takich "zakręconych" (dosłownie i przenośni) relacji między obiektami ;P