Szukaj…


Uwagi

W Javie obiekty są przydzielane do sterty, a pamięć sterty jest odzyskiwana przez automatyczne odśmiecanie. Aplikacja nie może jawnie usunąć obiektu Java.

Podstawowe zasady zbierania śmieci w Javie są opisane w przykładzie Zbieranie śmieci . Inne przykłady opisują finalizację, ręczne uruchamianie modułu wyrzucania elementów bezużytecznych i problem wycieków pamięci.

Finalizacja

Obiekt Java może zadeklarować metodę finalize . Ta metoda jest wywoływana tuż przed zwolnieniem przez Java pamięci dla obiektu. Zwykle będzie wyglądać tak:

public class MyClass {
  
    //Methods for the class

    @Override
    protected void finalize() throws Throwable {
        // Cleanup code
    }
}

Istnieją jednak pewne ważne zastrzeżenia dotyczące sposobu finalizacji Java.

  • Java nie gwarantuje, kiedy zostanie wywołana metoda finalize() .
  • Java nawet nie gwarantuje, że metoda finalize() zostanie wywołana przez jakiś czas w czasie działania uruchomionej aplikacji.
  • Jedyną gwarancją jest to, że metoda zostanie wywołana przed usunięciem obiektu ... jeśli obiekt zostanie usunięty.

Powyższe zastrzeżenia oznaczają, że złym pomysłem jest poleganie na metodzie finalize celu wykonania czynności czyszczenia (lub innych), które należy wykonać w odpowiednim czasie. Nadmierne poleganie na finalizacji może prowadzić do wycieków pamięci, wycieków pamięci i innych problemów.

Krótko mówiąc, jest bardzo niewiele sytuacji, w których finalizacja jest tak naprawdę dobrym rozwiązaniem.

Finalizatory działają tylko raz

Zwykle obiekt jest usuwany po sfinalizowaniu. Jednak nie zdarza się to cały czas. Rozważ następujący przykład 1 :

public class CaptainJack {
    public static CaptainJack notDeadYet = null;

    protected void finalize() {
        // Resurrection!
        notDeadYet = this;
    }
}

Gdy wystąpienie CaptainJack staje się nieosiągalne, a śmieciarz próbuje go odzyskać, metoda finalize() przypisze odwołanie do instancji do zmiennej notDeadYet . To sprawi, że instancja będzie osiągalna jeszcze raz, a śmieciarz go nie usunie.

Pytanie: Czy kapitan Jack jest nieśmiertelny?

Odpowiedź: Nie.

Chwytanie polega na tym, że JVM uruchomi finalizator na obiekcie tylko raz w swoim życiu. Jeśli przypiszesz null do notDeadYet powodując, że ponownie utworzona instancja będzie nieosiągalna, śmieciarz nie wywoła finalize() na obiekcie.

1 - Zobacz https://en.wikipedia.org/wiki/Jack_Harkness .

Ręczne uruchamianie GC

Możesz ręcznie uruchomić Garbage Collector, dzwoniąc

System.gc();

Jednak Java nie gwarantuje, że Garbage Collector uruchomi się po powrocie połączenia. Ta metoda po prostu „sugeruje” JVM (Java Virtual Machine), że ma on uruchamiać moduł wyrzucający elementy bezużyteczne, ale nie zmusza go do tego.

Powszechnie uważa się za złą praktykę próbę ręcznego uruchomienia wyrzucania elementów bezużytecznych. JVM można uruchomić z opcją -XX:+DisableExplicitGC , aby wyłączyć wywołania System.gc() . Wyzwalanie funkcji wyrzucania elementów bezużytecznych przez wywołanie System.gc() może zakłócać normalne działania związane z zarządzaniem / promocją obiektów w konkretnej implementacji modułu wyrzucania elementów bezużytecznych używane przez JVM.

Zbieranie śmieci

Podejście C ++ - nowe i usuń

W języku, takim jak C ++, aplikacja odpowiada za zarządzanie pamięcią wykorzystywaną przez pamięć przydzielaną dynamicznie. Gdy obiekt jest tworzony na stercie C ++ przy użyciu new operatora, musi istnieć odpowiednie użycie operatora delete celu usunięcia obiektu:

  • Jeśli program zapomni delete obiekt i po prostu „zapomni” o nim, powiązana pamięć zostanie utracona przez aplikację. Terminem na tę sytuację jest wyciek pamięci , a zbyt wiele wycieków pamięci może spowodować , że aplikacja będzie wykorzystywać coraz więcej pamięci i ostatecznie ulegnie awarii.

  • Z drugiej strony, jeśli aplikacja spróbuje dwukrotnie delete ten sam obiekt lub użyć obiektu po jego usunięciu, wówczas aplikacja może ulec awarii z powodu problemów z uszkodzeniem pamięci

W skomplikowanym programie C ++ zaimplementowanie zarządzania pamięcią przy użyciu new i delete może być czasochłonne. Rzeczywiście, zarządzanie pamięcią jest częstym źródłem błędów.

Podejście Java - odśmiecanie

Java ma inne podejście. Zamiast jawnego operatora delete , Java zapewnia automatyczny mechanizm znany jako odśmiecanie pamięci w celu odzyskania pamięci używanej przez obiekty, które nie są już potrzebne. System wykonawczy Java bierze odpowiedzialność za znalezienie obiektów, które mają zostać zutylizowane. To zadanie jest wykonywane przez składnik zwany śmieciarzem lub w skrócie GC.

W dowolnym momencie podczas wykonywania programu Java możemy podzielić zestaw wszystkich istniejących obiektów na dwa odrębne podzbiory 1 :

  • Obiekty osiągalne są definiowane przez JLS w następujący sposób:

    Obiekt osiągalny to każdy obiekt, do którego można uzyskać dostęp w dowolnym potencjalnym ciągłym obliczeniu z dowolnego aktywnego wątku.

    W praktyce oznacza to, że istnieje łańcuch odwołań zaczynający się od zmiennej lokalnej w zakresie lub zmiennej static , dzięki której jakiś kod może dotrzeć do obiektu.

  • Nieosiągalne obiekty to obiekty, do których nie można dotrzeć w sposób opisany powyżej.

Wszelkie obiekty, które są nieosiągalne, kwalifikują się do odśmiecania. Nie oznacza to, że będą zbierane śmieci. W rzeczywistości:

  • Nieosiągalny przedmiot nie jest gromadzony natychmiast po tym, jak staje się nieosiągalny 1 .
  • Nieosiągalny obiekt nie może być nigdy gromadzony.

Specyfikacja języka Java zapewnia dużą swobodę implementacji JVM w podejmowaniu decyzji, kiedy należy gromadzić nieosiągalne obiekty. Daje również (w praktyce) pozwolenie, aby implementacja JVM była konserwatywna w sposobie wykrywania nieosiągalnych obiektów.

Jedną rzeczą, którą JLS gwarantuje, jest to, że żadne osiągalne obiekty nigdy nie będą zbierane śmieci.

Co dzieje się, gdy obiekt staje się nieosiągalny

Przede wszystkim nic się nie dzieje, gdy obiekt staje się nieosiągalny. Dzieje się tak tylko wtedy, gdy moduł śmieciowy działa i wykrywa, że obiekt jest nieosiągalny. Co więcej, w przebiegu GC często nie wykrywa się wszystkich nieosiągalnych obiektów.

Gdy GC wykryje nieosiągalny obiekt, mogą wystąpić następujące zdarzenia.

  1. Jeśli istnieją obiekty Reference które odnoszą się do obiektu, odniesienia te zostaną usunięte przed usunięciem obiektu.

  2. Jeśli obiekt można sfinalizować , zostanie on sfinalizowany. Dzieje się to przed usunięciem obiektu.

  3. Obiekt można usunąć, a zajmowaną przez niego pamięć można odzyskać.

Zauważ, że istnieje wyraźna sekwencja, w której mogą wystąpić powyższe zdarzenia, ale nic nie wymaga od śmieciarza wykonania ostatecznego usunięcia dowolnego określonego obiektu w określonych ramach czasowych.

Przykłady osiągalnych i nieosiągalnych obiektów

Rozważ następujące przykładowe klasy:

// A node in simple "open" linked-list.
public class Node {
    private static int counter = 0;

    public int nodeNumber = ++counter;
    public Node next;
}

public class ListTest {
    public static void main(String[] args) {
        test();                    // M1
        System.out.prinln("Done"); // M2
    }
    
    private static void test() {
        Node n1 = new Node();      // T1
        Node n2 = new Node();      // T2
        Node n3 = new Node();      // T3
        n1.next = n2;              // T4
        n2 = null;                 // T5
        n3 = null;                 // T6
    }
}

Przeanalizujmy, co się dzieje, gdy wywoływana jest metoda test() . Instrukcje T1, T2 i T3 tworzą obiekty Node , a wszystkie obiekty są osiągalne poprzez odpowiednio zmienne n1 , n2 i n3 . Instrukcja T4 przypisuje odwołanie do obiektu 2nd Node do next pola pierwszego. Gdy to nastąpi, do drugiego Node można dotrzeć dwiema ścieżkami:

 n2 -> Node2
 n1 -> Node1, Node1.next -> Node2

W instrukcji T5 przypisujemy null do n2 . To zrywa pierwszy z łańcuchów osiągalności dla Node2 , ale drugi pozostaje nieprzerwany, więc Node2 jest nadal osiągalny.

W instrukcji T6 przypisujemy null do n3 . To zrywa jedyny łańcuch osiągalności dla Node3 , co powoduje, że Node3 nieosiągalny. Jednak Node1 i Node2 są zarówno nadal osiągalny poprzez n1 zmiennej.

Wreszcie, gdy metoda test() powróci, jej zmienne lokalne n1 , n2 i n3 wykraczają poza zakres i dlatego nie mogą być dostępne przez nic. Te przerwy pozostałe łańcuchy osiągalności dla Node1 i Node2 , a wszystkie z Node obiektów są ani nieosiągalny i kwalifikuje się do zbierania śmieci.


1 - Jest to uproszczenie, które ignoruje finalizację i klasy Reference . 2 - Hipotetycznie implementacja Java mogłaby to zrobić, ale koszt wydajności takiego działania sprawia, że jest to niepraktyczne.

Ustawianie rozmiarów stosu, trwałości i stosu

Po uruchomieniu maszyny wirtualnej Java musi wiedzieć, jak duży jest stos i domyślny rozmiar stosów wątków. Można je określić za pomocą opcji wiersza polecenia w poleceniu java . W przypadku wersji Java wcześniejszych niż Java 8 można również określić rozmiar regionu PermGen stosu.

Zauważ, że PermGen został usunięty w Javie 8 i jeśli spróbujesz ustawić rozmiar PermGen, opcja zostanie zignorowana (z komunikatem ostrzegawczym).

Jeśli nie określisz jawnie rozmiarów sterty i stosu, JVM użyje wartości domyślnych obliczanych w sposób zależny od wersji i platformy. Może to spowodować, że aplikacja zużyje za mało lub za dużo pamięci. Zazwyczaj jest to OK w przypadku stosów wątków, ale może być problematyczne w przypadku programu, który wykorzystuje dużo pamięci.

Ustawianie sterty, PermGen i domyślnych rozmiarów stosów:

Następujące opcje JVM ustawiają rozmiar sterty:

  • -Xms<size> - ustawia początkowy rozmiar sterty
  • -Xmx<size> - ustawia maksymalny rozmiar sterty
  • -XX:PermSize<size> - ustawia początkowy rozmiar PermGen
  • -XX:MaxPermSize<size> - ustawia maksymalny rozmiar PermGen
  • -Xss<size> - ustawia domyślny rozmiar stosu wątków

Parametr <size> może być liczbą bajtów lub sufiksem k , m lub g . Te ostatnie określają rozmiar odpowiednio w kilobajtach, megabajtach i gigabajtach.

Przykłady:

$ java -Xms512m -Xmx1024m JavaApp
$ java -XX:PermSize=64m -XX:MaxPermSize=128m JavaApp
$ java -Xss512k JavaApp

Znajdowanie domyślnych rozmiarów:

Opcji -XX:+printFlagsFinal można użyć do wydrukowania wartości wszystkich flag przed uruchomieniem JVM. Można to wykorzystać do wydrukowania ustawień domyślnych dla stosu i ustawień rozmiaru stosu w następujący sposób:

  • W systemach Linux, Unix, Solaris i Mac OSX

    $ java -XX: + PrintFlagsFinal -version | grep -iE 'HeapSize | PermSize | ThreadStackSize'

  • Dla Windowsa:

    java -XX: + PrintFlagsFinal -version | findstr / i „HeapSize PermSize ThreadStackSize”

Dane wyjściowe powyższych poleceń będą przypominać następujące:

uintx InitialHeapSize                          := 20655360        {product}
uintx MaxHeapSize                              := 331350016       {product}
uintx PermSize                                  = 21757952        {pd product}
uintx MaxPermSize                               = 85983232        {pd product}
 intx ThreadStackSize                           = 1024            {pd product}

Rozmiary podano w bajtach.

Wycieki pamięci w Javie

W przykładzie Garbage collection sugerujemy, że Java rozwiązuje problem wycieków pamięci. To nie jest tak naprawdę prawda. Program Java może przeciekać pamięć, chociaż przyczyny wycieków są raczej różne.

Przedmioty osiągalne mogą przeciekać

Rozważ następującą naiwną implementację stosu.

public class NaiveStack {
    private Object[] stack = new Object[100];
    private int top = 0;

    public void push(Object obj) {
        if (top >= stack.length) {
            throw new StackException("stack overflow");
        }
        stack[top++] = obj;
    }

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        return stack[--top];
    }

    public boolean isEmpty() {
        return top == 0;
    }
}

Kiedy push obiekt, a następnie natychmiast go pop , nadal będzie referencja do obiektu w tablicy stack .

Logika implementacji stosu oznacza, że tego odwołania nie można zwrócić klientowi interfejsu API. Jeśli obiekt został pęknięty, możemy udowodnić, że nie można „uzyskać do niego dostępu w żadnym potencjalnym ciągłym obliczeniu z dowolnego aktywnego wątku” . Problem polega na tym, że JVM obecnej generacji nie może tego udowodnić. Maszyny JVM bieżącej generacji nie uwzględniają logiki programu przy ustalaniu, czy można uzyskać odwołania. (Na początek nie jest to praktyczne).

Ale pomijając kwestię tego, co naprawdę oznacza osiągalność , najwyraźniej mamy tutaj sytuację, w której implementacja NaiveStackNaiveStack się” na obiektach, które należy odzyskać. To przeciek pamięci.

W takim przypadku rozwiązanie jest proste:

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        Object popped = stack[--top];
        stack[top] = null;              // Overwrite popped reference with null.
        return popped;
    }

Pamięć podręczna może być wyciekiem pamięci

Powszechną strategią poprawy wydajności usług jest buforowanie wyników. Chodzi o to, aby przechowywać typowe żądania i ich wyniki w strukturze danych w pamięci zwanej pamięcią podręczną. Następnie za każdym razem, gdy zostanie wykonane żądanie, przeszukujesz je w pamięci podręcznej. Jeśli wyszukiwanie powiedzie się, zwracane są odpowiednie zapisane wyniki.

Ta strategia może być bardzo skuteczna, jeśli zostanie odpowiednio wdrożona. Jednak niepoprawnie zaimplementowana pamięć podręczna może być przeciekiem pamięci. Rozważ następujący przykład:

public class RequestHandler {
    private Map<Task, Result> cache = new HashMap<>();

    public Result doRequest(Task task) {
        Result result = cache.get(task);
        if (result == null) {
            result == doRequestProcessing(task);
            cache.put(task, result);
        }
        return result;
    }
}

Problem z tym kodem polega na tym, że chociaż każde wywołanie doRequest może dodać nowy wpis do pamięci podręcznej, nie można ich usunąć. Jeśli usługa stale otrzymuje różne zadania, pamięć podręczna ostatecznie zużyje całą dostępną pamięć. Jest to forma wycieku pamięci.

Jednym ze sposobów rozwiązania tego problemu jest użycie pamięci podręcznej o maksymalnym rozmiarze i wyrzucanie starych wpisów, gdy pamięć podręczna przekroczy maksimum. (Wyrzucenie ostatnio używanego wpisu jest dobrą strategią.) Innym podejściem jest zbudowanie pamięci podręcznej przy użyciu WeakHashMap aby JVM mogła eksmitować wpisy pamięci podręcznej, jeśli sterty zaczną się WeakHashMap .



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow