Suche…


Bemerkungen

In Java werden Objekte im Heapspeicher zugewiesen, und der Heapspeicher wird durch die automatische Garbage Collection wiederhergestellt. Ein Anwendungsprogramm kann ein Java-Objekt nicht explizit löschen.

Die Grundprinzipien der Java Garbage Collection werden im Garbage Collection- Beispiel beschrieben. Andere Beispiele beschreiben die Finalisierung, wie man den Garbage Collector von Hand auslöst, und das Problem von Speicherlecks.

Finalisierung

Ein Java-Objekt kann eine finalize Methode deklarieren. Diese Methode wird aufgerufen, kurz bevor Java den Speicher für das Objekt freigibt. Es wird normalerweise so aussehen:

public class MyClass {
  
    //Methods for the class

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

Es gibt jedoch einige wichtige Einschränkungen beim Verhalten von Java-Finalisierungen.

  • Java übernimmt keine Garantie dafür, wann eine finalize() -Methode aufgerufen wird.
  • Java garantiert nicht einmal, dass eine finalize() -Methode einige Zeit während der Laufzeit der laufenden Anwendung aufgerufen wird.
  • Die einzige Sache, die garantiert ist, ist, dass die Methode aufgerufen wird, bevor das Objekt gelöscht wird ... wenn das Objekt gelöscht wird.

Die oben genannten Einschränkungen bedeuten, dass es keine gute Idee ist, sich auf die finalize Methode zu verlassen, um Bereinigungs- (oder andere) Aktionen auszuführen, die rechtzeitig ausgeführt werden müssen. Ein zu starkes Vertrauen in die Finalisierung kann zu Speicherverlusten, Speicherverlusten und anderen Problemen führen.

Kurz gesagt, es gibt nur wenige Situationen, in denen die Fertigstellung tatsächlich eine gute Lösung darstellt.

Finalizer laufen nur einmal

Normalerweise wird ein Objekt gelöscht, nachdem es abgeschlossen wurde. Dies passiert jedoch nicht immer. Betrachten Sie das folgende Beispiel 1 :

public class CaptainJack {
    public static CaptainJack notDeadYet = null;

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

Wenn eine Instanz von CaptainJack mehr erreichbar ist und der Garbage Collector versucht, diese zurückzufordern, weist die Methode notDeadYet finalize() der notDeadYet Variablen einen Verweis auf die Instanz zu. Dadurch wird die Instanz erneut erreichbar, und der Garbage Collector löscht sie nicht.

Frage: Ist Captain Jack unsterblich?

Antwort: Nein

Der Haken ist, dass die JVM einen Finalizer für ein Objekt nur einmal im Leben ausführt. Wenn Sie notDeadYet Wert null notDeadYet sodass eine wiederhergestellte Instanz erneut nicht erreichbar ist, ruft der Garbage Collector nicht finalize() für das Objekt auf.

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

GC manuell auslösen

Sie können den Garbage Collector manuell auslösen, indem Sie ihn aufrufen

System.gc();

Java garantiert jedoch nicht, dass der Garbage Collector ausgeführt wurde, wenn der Aufruf zurückkehrt. Diese Methode "schlägt" der JVM (Java Virtual Machine) einfach vor, dass sie den Garbage Collector ausführen soll, zwingt sie jedoch nicht dazu.

Es wird im Allgemeinen als schlechte Praxis betrachtet, zu versuchen, die Garbage Collection manuell auszulösen. Die JVM kann mit der Option -XX:+DisableExplicitGC , um Aufrufe von System.gc() zu deaktivieren. Das Auslösen der Garbage Collection durch Aufrufen von System.gc() kann die normalen Garbage Management / Object Promotion-Aktivitäten der von der JVM verwendeten spezifischen Garbage Collection- System.gc() .

Müllsammlung

Der C ++ - Ansatz - neu und löschen

In einer Sprache wie C ++ ist das Anwendungsprogramm dafür verantwortlich, den von dynamisch zugewiesenen Speicher verwendeten Speicher zu verwalten. Wenn ein Objekt mit dem new Operator im C ++ - Heap erstellt wird, muss der delete Operator entsprechend verwendet werden, um das Objekt zu entsorgen:

  • Wenn das Programm das delete eines Objekts vergisst und nur das Objekt "vergisst", geht der zugehörige Speicher für die Anwendung verloren. Der Begriff für diese Situation ist ein Speicherverlust , und zu viel Speicherverlust, so dass eine Anwendung mehr und mehr Speicher benötigt und schließlich abstürzt.

  • Wenn eine Anwendung andererseits versucht, dasselbe Objekt zweimal zu delete oder ein Objekt zu verwenden, nachdem es gelöscht wurde, kann die Anwendung aufgrund von Problemen mit der Speicherbeschädigung abstürzen

In einem komplizierten C ++ - Programm kann das Implementieren der Speicherverwaltung mit new und delete zeitaufwändig sein. In der Tat ist die Speicherverwaltung eine häufige Fehlerquelle.

Der Java-Ansatz - Speicherbereinigung

Java verfolgt einen anderen Ansatz. Anstelle eines expliziten delete stellt Java einen automatischen Mechanismus bereit, der als Garbage Collection bezeichnet wird, um den Speicher wiederzuerlangen, der von nicht mehr benötigten Objekten beansprucht wird. Das Java-Laufzeitsystem übernimmt die Verantwortung für das Auffinden der zu entsorgenden Objekte. Diese Aufgabe wird von einer Komponente ausgeführt, die als Garbage Collector oder kurz GC bezeichnet wird.

Während der Ausführung eines Java-Programms können Sie jederzeit die Menge aller vorhandenen Objekte in zwei unterschiedliche Teilmengen 1 aufteilen:

  • Erreichbare Objekte werden von der JLS wie folgt definiert:

    Ein erreichbares Objekt ist jedes Objekt, auf das bei jeder möglichen fortlaufenden Berechnung von einem beliebigen Live-Thread aus zugegriffen werden kann.

    In der Praxis bedeutet dies, dass es eine Kette von Verweisen gibt, die von einer lokalen Variablen im Gültigkeitsbereich oder einer static Variablen ausgehen, über die ein Code das Objekt erreichen kann.

  • Nicht erreichbare Objekte sind Objekte, die nicht wie oben beschrieben erreichbar sind.

Alle Objekte, die nicht erreichbar sind, können für die Garbage Collection verwendet werden. Dies bedeutet nicht , dass sie Müll gesammelt werden. Eigentlich:

  • Ein nicht erreichbares Objekt wird nicht sofort abgerufen, wenn es nicht erreichbar ist 1 .
  • Ein unerreichbarer Gegenstand wird möglicherweise niemals als Müll gesammelt.

Die Java-Sprachspezifikation gibt einer JVM-Implementierung viel Spielraum, um zu entscheiden, wann nicht erreichbare Objekte erfasst werden sollen. In der Praxis wird auch die Erlaubnis erteilt, dass eine JVM-Implementierung bei der Erkennung nicht erreichbarer Objekte konservativ ist.

Die JLS garantiert nur, dass keine erreichbaren Objekte gesammelt werden.

Was passiert, wenn ein Objekt unerreichbar wird?

Zunächst einmal geschieht nichts gesagt , wenn ein Objekt nicht mehr erreichbar ist. Dies geschieht nur, wenn der Garbage Collector ausgeführt wird und erkennt, dass das Objekt nicht erreichbar ist. Es ist außerdem üblich, dass ein GC-Lauf nicht alle nicht erreichbaren Objekte erkennt.

Wenn der GC ein nicht erreichbares Objekt erkennt, können die folgenden Ereignisse auftreten.

  1. Wenn Reference sind, die auf das Objekt verweisen, werden diese Referenzen gelöscht, bevor das Objekt gelöscht wird.

  2. Wenn das Objekt finalizable ist, dann wird es fertig gestellt sein. Dies geschieht, bevor das Objekt gelöscht wird.

  3. Das Objekt kann gelöscht werden und der Speicher, den es belegt, kann zurückgefordert werden.

Beachten Sie, dass es eine klare Reihenfolge gibt, in der die obigen Ereignisse auftreten können. Der Garbage Collector muss jedoch nicht die endgültige Löschung eines bestimmten Objekts in einem bestimmten Zeitraum durchführen.

Beispiele für erreichbare und nicht erreichbare Objekte

Betrachten Sie die folgenden Beispielklassen:

// 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
    }
}

Lassen Sie uns untersuchen, was passiert, wenn test() aufgerufen wird. Die Anweisungen T1, T2 und T3 erzeugen Node , und die Objekte sind alle über die Variablen n1 , n2 und n3 erreichbar. Anweisung T4 weist dem next Feld des ersten Feldes die Referenz auf das 2. Node zu. Node ist der 2. Node über zwei Pfade erreichbar:

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

In Anweisung T5 weisen wir n2 null zu. Dadurch wird die erste der Erreichbarkeitsketten für Node2 , die zweite bleibt jedoch ungebrochen, sodass Node2 weiterhin erreichbar ist.

In Anweisung T6 weisen wir n3 null zu. Dies Node3 die einzige Erreichbarkeitskette für Node3 , wodurch Node3 nicht erreichbar ist. Node1 und Node2 sind jedoch beide weiterhin über die Variable n1 erreichbar.

Wenn die test() -Methode zurückkehrt, gehen ihre lokalen Variablen n1 , n2 und n3 schließlich aus dem Gültigkeitsbereich heraus und sind daher für nichts zugänglich. Dies bricht die verbleibenden Erreichbarkeitsketten für Node1 und Node2 , und alle der Node Objekte sind noch nicht erreichbar und kommen für die Garbage Collection.


1 - Dies ist eine Vereinfachung, die Finalisierungs- und Reference ignoriert. 2 - Hypothetisch könnte eine Java-Implementierung dies tun, aber der damit verbundene Performance-Aufwand macht dies unpraktisch.

Festlegen der Heap-, PermGen- und Stack-Größen

Wenn eine virtuelle Java-Maschine gestartet wird, muss sie wissen, wie groß der Heap ist, und die Standardgröße für Thread-Stacks. Diese können mithilfe von Befehlszeilenoptionen im java Befehl angegeben werden. Bei Java-Versionen vor Java 8 können Sie auch die Größe der PermGen-Region des Heap angeben.

Beachten Sie, dass PermGen in Java 8 entfernt wurde. Wenn Sie versuchen, die PermGen-Größe festzulegen, wird die Option ignoriert (mit einer Warnmeldung).

Wenn Sie die Heap- und Stack-Größen nicht explizit angeben, verwendet die JVM Standardwerte, die auf einer Version und plattformspezifisch berechnet werden. Dies kann dazu führen, dass Ihre Anwendung zu wenig oder zu viel Speicher verwendet. Dies ist normalerweise für Thread-Stacks in Ordnung, für ein Programm, das viel Speicher benötigt, kann dies jedoch problematisch sein.

Festlegen der Heap-, PermGen- und Standard-Stack-Größen:

Die folgenden JVM-Optionen legen die Heapgröße fest:

  • -Xms<size> - legt die anfängliche Größe des -Xms<size>
  • -Xmx<size> - -Xmx<size> die maximale Größe des -Xmx<size>
  • -XX:PermSize<size> - Legt die anfängliche PermGen-Größe fest
  • -XX:MaxPermSize<size> - Legt die maximale PermGen-Größe fest
  • -Xss<size> - -Xss<size> die Standard- -Xss<size>

Der <size> -Parameter kann eine Anzahl von Bytes sein oder ein Suffix von k , m oder g . Letztere geben die Größe in Kilobyte, Megabyte und Gigabyte an.

Beispiele:

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

Ermitteln der Standardgrößen:

Mit der Option -XX:+printFlagsFinal können Sie die Werte aller Flags drucken, bevor Sie die JVM starten. Dies kann verwendet werden, um die Standardeinstellungen für die Einstellungen für Heap und Stapelgröße wie folgt zu drucken:

  • Für Linux, Unix, Solaris und Mac OSX

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

  • Für Windows:

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

Die Ausgabe der obigen Befehle ähnelt der folgenden:

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

Die Größen werden in Bytes angegeben.

Speicherlecks in Java

Im Beispiel für die Garbage-Sammlung wurde impliziert, dass Java das Problem von Speicherverlusten löst. Das stimmt eigentlich nicht. Ein Java-Programm kann Speicher verlieren, obwohl die Ursachen der Lecks ziemlich unterschiedlich sind.

Erreichbare Objekte können auslaufen

Betrachten Sie die folgende naive Stack-Implementierung.

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;
    }
}

Wenn Sie ein Objekt push und es sofort wieder pop , wird immer noch ein Verweis auf das Objekt im stack Array angezeigt.

Die Logik der Stack-Implementierung bedeutet, dass diese Referenz nicht an einen Client der API zurückgegeben werden kann. Wenn ein Objekt geknackt wurde, können wir nachweisen, dass es "nicht in einer möglichen fortlaufenden Berechnung von einem Live-Thread aus zugänglich ist" . Das Problem ist, dass eine JVM der aktuellen Generation dies nicht beweisen kann. JVMs der aktuellen Generation berücksichtigen die Logik des Programms nicht bei der Bestimmung, ob Referenzen erreichbar sind. (Zunächst einmal ist es nicht praktisch.)

Abgesehen von der Frage, was Erreichbarkeit wirklich bedeutet, haben wir hier eindeutig eine Situation, in der die NaiveStack Implementierung an Objekten "hängt", die zurückgefordert werden müssen. Das ist ein Speicherleck.

In diesem Fall ist die Lösung einfach:

    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;
    }

Caches können Speicherlecks sein

Eine übliche Strategie zur Verbesserung der Serviceleistung ist das Zwischenspeichern von Ergebnissen. Die Idee ist, dass Sie häufige Anforderungen und ihre Ergebnisse in einer Datenstruktur im Arbeitsspeicher speichern, die als Cache bezeichnet wird. Jedes Mal, wenn eine Anforderung erfolgt, suchen Sie die Anforderung im Cache nach. Wenn die Suche erfolgreich ist, geben Sie die entsprechenden gespeicherten Ergebnisse zurück.

Diese Strategie kann sehr effektiv sein, wenn sie richtig umgesetzt wird. Bei falscher Implementierung kann ein Cache jedoch ein Speicherverlust sein. Betrachten Sie das folgende Beispiel:

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;
    }
}

Das Problem bei diesem Code besteht darin, dass der Aufruf von doRequest zwar einen neuen Eintrag zum Cache hinzufügen kann, jedoch nicht entfernt werden kann. Wenn der Dienst ständig andere Aufgaben erhält, verbraucht der Cache schließlich den gesamten verfügbaren Speicherplatz. Dies ist eine Form von Speicherverlust.

Ein Lösungsansatz besteht darin, einen Cache mit einer maximalen Größe zu verwenden und alte Einträge zu verwerfen, wenn der Cache den Maximalwert überschreitet. (Das Löschen des am wenigsten verwendeten Eintrags ist eine gute Strategie.) Ein anderer Ansatz besteht darin, den Cache mit WeakHashMap sodass die JVM Cache-Einträge WeakHashMap kann, wenn der WeakHashMap zu voll wird.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow