25 czerwca 2008

SCJP - Odśmiecacz, czyli mechanizm przywracania pamięci

Dzisiejszym artykułem zamkniemy wreszcie trzeci rozdział Książki – patrz artykuł "Przygotowania do SCJP czas zacząć". Jak sami autorzy piszą, był to rozdział monstrum; cieszę się więc, że mamy to już za sobą. Kolejny rozdział dla odmiany jest króciutki. Będzie okazja poprawić sobie humor, że nie idzie to aż tak wolno. Będzie dzisiaj o odśmiecaniu pamięci (ang. garbage collection).

Obiekty w Javie, tak jak wszystko w przyrodzie mają swój początek i koniec. Cykl jest prosty – tworzymy, używamy i… porzucamy. Zwróćmy uwagę na ostatnie słowo; porzucamy, ale nie niszczymy. Niszczeniem zajmuje się odśmiecacz (ang. garbage collector). Naturalnie, odśmiecacz niszczy tylko obiekty porzucone, a więc takie, które już nigdy nie będą mogły zostać użyte. Ktoś mógłby powiedzieć, że niepotrzebne obiekty to takie, na które nie wskazuje żadna referencja. Jest to prawda, ale tylko częściowa. Rzeczywiście, obiekty na które nie wskazuje żadna referencja są już bezużyteczne i podlegają odśmiecaniu, ale nie jest to warunek konieczny. Zerknijmy na poniższy przykład.

public class Test {
public static void main(String[] args) {
SomeClass oneInstance = new SomeClass();

oneInstance.otherInstance = new SomeClass();

oneInstance = null;
}
}

class SomeClass {
public SomeClass otherInstance;
}

W pierwszej linii metody ‘main(…)’ tworzymy nową instancję klasy ‘SomeClass’ i przypisujemy ją do zadeklarowanej referencji. W drugiej linii tworzymy kolejny obiekt i przypisujemy go do zmiennej instancyjnej obiektu utworzonego uprzednio. Zauważmy, że póki co oba obiekty są osiągalne z aktywnego wątku aplikacji – pierwszy obiekt jest osiągalny bezpośrednio, poprzez referencję ‘oneInstance’ a drugi pośrednio. W trzeciej linii metody ‘main(…)’ przypisujemy do zmiennej ‘oneInstance’ wartość ‘null’. Tym samym powodujemy, że żadna z dwu utworzonych uprzednio instancji nie jest już osiągalna z aktywnego wątku aplikacji. Co prawda żadna referencja nie wskazuje już na obiekt utworzony jako pierwszy, ale na drugi z obiektów cały czas wskazuje referencja ‘otherInstance’. Mimo to, oba obiekty są już bezużyteczne i podlegają procesowi odśmiecania. Definicja obiektu porzuconego, a więc podlegającego procesowi odśmiecania jest więc taka, że jest to obiekt który nie jest bezpośrednio ani pośrednio osiągalny z żadnego aktywnego wątku wykonania.

Pozostało jeszcze odpowiedzieć na pytanie – kiedy uruchamiany jest proces odśmiecacza? Generalnie rzecz biorąc… nie wiadomo – zależy to od konkretnej implementacji JVM. Co prawda mamy do dyspozycji metodę ‘java.lang.Runtime.getRuntime().gc()’, która służy do zgłoszenia prośby o uruchamianie odśmiecania, ale to tylko tyle – zgłoszenie prośby. Wywołanie tej metody wcale nie musi zakończyć się podjęciem jakiejkolwiek akcji, jest to tylko drobna sugestia, którą kierujemy do JVM, jednak, gdy już JVM zdecyduje się na podjęcie akcji oczyszczania pamięci to robi to synchronicznie – proces kończy się, zanim nastąpi powrót z wywołania. Ekwiwalentem dla wywołania ‘java.lang.Runtime.getRuntime().gc()’ jest ‘java.lang.System.gc()’.

Ze względu na brak możliwości założenia czegokolwiek na temat sposobu działania i momentu uruchomienia odśmiecacza – oraz braku gwarancji, że dany obiekt w ogóle zostanie kiedykolwiek usunięty z pamięci – język Java nie oferuje znanych z skądinąd destruktorów. Oferuje jednak pewną namiastkę – metodę ‘finalize()’. Jest to metoda zdefiniowana w klasie ‘Object’. Nie jest to metoda finalna, więc może być nadpisana (ang. overridden) w dowolnej podklasie. Znaczenie tej metody jest zbliżone do destruktorów w tym sensie, że jest to metoda wywoływana automatycznie przez proces odśmiecacza tuż przez usunięciem obiektu z pamięci, tyle, że jest jedno ale. Spójrzmy na poniższy przykład.

public class ResurrectionClass {
public static Set<Object> collection;
}

class SomeClass {

@Override
public void finalize() {
ResurrectionClass.collection.add(this);
}
}

Kod ten jest jak najbardziej poprawny. Jako że metoda ‘finalize()’ to metoda jak każda inna, to możliwe jest umieszczenie w niej kodu, który powoduje, że dana instancja zaczyna być osiągalna z aktywnego wątku a więc nie może być usunięta. I nie jest. Nie czyni to jednak instancji tej klasy nieśmiertelnymi, bowiem metoda ‘finalize()’ wywoływana jest dla każdego usuwanego obiektu tylko i wyłącznie raz! Proces odśmiecania wywoła metodę ‘finalize()’ za pierwszym razem, gdy próbuje usunąć obiekt z pamięci i wtedy możliwe jest wykonanie operacji pokazanej powyżej, jednak już za drugim razem metoda ta nie jest wywoływana i obiekt jest nieuchronnie niszczony.

8 komentarzy:

Koziołek pisze...

Ej! A co z algorytmami gc i konstrukcją pamięci? Tak wiem nie jest to na SCJP poruszane, ale szczególnie konstrukcja pamięci jest bardzo ważna. Wbrew pozorom w javie są możliwe wycieki pamięci.

Mariusz Lipiński pisze...

A masz może jakiś link do artykułu w którym jest napisane coś więcej na te tematy? Tak jak napisałeś, jest to poza zakresem SCJP, ale chętnie bym przeczytał. A może sam napiszesz taki artykuł?

Pozdr. Mariusz

Anonimowy pisze...

Hmm, mógłbyś nieco bardziej wyjaśnić dlaczego w tym ostatnim przypadku obiekty tej klasy nie są nieśmiertelne?

Przecież po wykonaniu metody finalize() do danego obiektu pojawia się referencja, więc chyba nie powinien być on usuwany ponieważ JVM widzi, że możemy się do niego odwołać.

Przygotowałem taki kod:

import java.util.ArrayList;

public class Res
{
public static ArrayList collection = new ArrayList();

public static void main(String ... args)
{
for(int i = 0; i < 1000000; i++)
new SomeClass();
System.out.println(collection.size());
}
}

class SomeClass {

@Override
public void finalize() {
Res.collection.add(this);
}
}

I przy uruchomieniu pojawia się losowa liczba, najczęściej z zakresu 500k - 800k. Więc wychodzi na to, że te obiekty z tej listy są usuwane. Dziwne... Mógłbyś to nieco wyjaśnić? Dzięki

Mariusz Lipiński pisze...

Tak jak pisałem "metoda ‘finalize()’ wywoływana jest dla każdego usuwanego obiektu tylko i wyłącznie raz". Odśmiecacz wywołuje finalize() za pierwszym razem gdy chce usunąć obiekt i zapamiętuje sobie jakoś, że dla tego konkretnego obiektu metoda ta została już wywołana. Za drugim razem, gdy zabiera się za usunięcie tego obiektu widzi, że finalize() została już wywołana więc jej nie wywołuje.

Anonimowy pisze...

No tak, ale przecież referencja do danego obiektu jest cały czas umieszczona w tej ArrayLiście, więc GC powinien uznać, że dany obiekt nie kwalifikuje się do odśmiecenia -> więc nie powinien go usunąć. A mimo to usuwa, dlaczego?

Anonimowy pisze...

Hyh?

Mariusz Lipiński pisze...

Tak, tak,

teraz rozumiem twoje pytanie. Niestety nie znam odpowiedzi a akurat zaczynam nowy projekt i nie mam czasu na poszukiwanie. Może ktoś z czytelników wie?

Pozdr. Mariusz

Mariusz Lipiński pisze...

No tak, wszystko jasne. Do ArrayListy dodały się tylko te obiekty, które GC próbował usunąć a więc rozmiar listy jest mniejszy niż 1000000, bo część obiektów nigdy nie była usuwana przez GC i nie miała szansy się do tej listy dodać. Obiektów klasy SomeClass jest więc 1000000, tyle, że nie wszystkie dodały się do listy.

Pozdr. Mariusz