Informationen zu Speicherverlusten in Java

Grundlegendes zu Speicherverlusten in Java

1. Einführung

Einer der Hauptvorteile von Java ist die automatisierte Speicherverwaltung mit Hilfe des integrierten Garbage Collector (oder kurzGC). Der GC kümmert sich implizit um die Zuweisung und Freigabe von Speicher und ist daher in der Lage, den Großteil der Speicherverluste zu beheben.

Der GC verarbeitet zwar effektiv einen guten Teil des Speichers, garantiert jedoch keine narrensichere Lösung für Speicherverluste. Der GC ist ziemlich schlau, aber nicht makellos. Speicherlecks können sich auch in Anwendungen eines gewissenhaften Entwicklers noch einschleichen.

Es kann immer noch Situationen geben, in denen die Anwendung eine erhebliche Anzahl überflüssiger Objekte generiert und so wichtige Speicherressourcen erschöpft, was manchmal zum Ausfall der gesamten Anwendung führt.

Speicherverluste sind ein echtes Problem in Java. In diesem Tutorial sehen wirwhat the potential causes of memory leaks are, how to recognize them at runtime, and how to deal with them in our application.

2. Was ist ein Speicherverlust?

Ein Speicherverlust ist eine Situationwhen there are objects present in the heap that are no longer used, but the garbage collector is unable to remove them from memory und wird daher unnötig aufrechterhalten.

Ein Speicherverlust ist schlecht, weil erblocks memory resources and degrades system performance over time beträgt. Und wenn dies nicht behandelt wird, erschöpft die Anwendung schließlich ihre Ressourcen und endet schließlich mit einem schwerwiegendenjava.lang.OutOfMemoryError.

Es gibt zwei verschiedene Arten von Objekten, die sich im Heapspeicher befinden - referenziert und nicht referenziert. Referenzierte Objekte sind Objekte, die noch aktive Referenzen in der Anwendung haben, während nicht referenzierte Objekte keine aktiven Referenzen haben.

The garbage collector removes unreferenced objects periodically, but it never collects the objects that are still being referenced. Hier können Speicherlecks auftreten:

 

image

Symptome eines Gedächtnisverlustes

  • Starker Leistungsabfall, wenn die Anwendung über einen längeren Zeitraum ununterbrochen ausgeführt wird

  • OutOfMemoryError Heap-Fehler in der Anwendung

  • Spontane und seltsame Anwendung stürzt ab

  • Der Anwendung gehen gelegentlich die Verbindungsobjekte aus

Schauen wir uns einige dieser Szenarien genauer an und wie man damit umgeht.

3. Arten von Speicherverlusten in Java

In jeder Anwendung können Speicherverluste aus zahlreichen Gründen auftreten. In diesem Abschnitt werden die häufigsten besprochen.

3.1. Speicherverlust durchstatic Felder

Das erste Szenario, das einen potenziellen Speicherverlust verursachen kann, ist die starke Verwendung vonstatic Variablen.

In Javastatic fields have a life that usually matches the entire lifetime of the running application (es sei denn,ClassLoader kann für die Speicherbereinigung verwendet werden).

Erstellen wir ein einfaches Java-Programm, dasstaticList: auffüllt

public class StaticTest {
    public static List list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }

    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

Wenn wir nun den Heap-Speicher während dieser Programmausführung analysieren, werden wir feststellen, dass sich der Heap-Speicher zwischen den Debug-Punkten 1 und 2 erwartungsgemäß erhöht hat.

Wenn wir jedoch diepopulateList()-Methode am Debugpunkt 3 belassen,the heap memory isn’t yet garbage collected, wie wir in dieser VisualVM-Antwort sehen können:

 

image

Wenn wir im obigen Programm in Zeile 2 jedoch nur das Schlüsselwortstatic löschen, führt dies zu einer drastischen Änderung der Speichernutzung. Diese Antwort von Visual VM zeigt Folgendes:

 

image

Der erste Teil bis zum Debug-Punkt entspricht fast dem, was wir im Fall vonstatic. erhalten haben. Diesmal jedoch, nachdem wir die MethodepopulateList() verlassen haben,all the memory of the list is garbage collected because we don’t have any reference to it.

Daher müssen wir sehr genau auf unsere Verwendung vonstatic Variablen achten. Wenn Sammlungen oder große Objekte alsstatic deklariert werden, verbleiben sie während der gesamten Lebensdauer der Anwendung im Speicher, wodurch der wichtige Speicher blockiert wird, der andernfalls anderweitig verwendet werden könnte.

Wie kann man das verhindern?

  • Minimieren Sie die Verwendung vonstatic Variablen

  • Verlassen Sie sich bei der Verwendung von Singletons auf eine Implementierung, die das Objekt träge lädt, anstatt es eifrig zu laden

3.2. Durch nicht geschlossene Ressourcen

Immer wenn wir eine neue Verbindung herstellen oder einen Stream öffnen, weist die JVM diesen Ressourcen Speicher zu. Einige Beispiele umfassen Datenbankverbindungen, Eingabestreams und Sitzungsobjekte.

Das Vergessen, diese Ressourcen zu schließen, kann den Speicher blockieren und sie somit außerhalb der Reichweite von GC halten. Dies kann sogar im Falle einer Ausnahme passieren, die verhindert, dass die Programmausführung die Anweisung erreicht, die den Code zum Schließen dieser Ressourcen verarbeitet.

In beiden Fällenthe open connection left from resources consumes memory, und wenn wir uns nicht mit ihnen befassen, können sie die Leistung beeinträchtigen und sogar zuOutOfMemoryError führen.

Wie kann man das verhindern?

  • Verwenden Sie immer den Blockfinally, um Ressourcen zu schließen

  • Der Code (auch imfinally-Block), der die Ressourcen schließt, sollte selbst keine Ausnahmen haben

  • Bei Verwendung von Java 7+ können wir den Blocktry-with-resources verwenden

3.3. Unsachgemäße Implementierungen vonequals() undhashCode()

Beim Definieren neuer Klassen besteht ein sehr häufiges Versehen darin, keine geeigneten überschriebenen Methoden für die Methodenequals() undhashCode() zu schreiben.

HashSet undHashMap verwenden diese Methoden in vielen Vorgängen. Wenn sie nicht korrekt überschrieben werden, können sie zu einer Quelle für potenzielle Speicherverlustprobleme werden.

Nehmen wir ein Beispiel für eine trivialePerson-Klasse und verwenden Sie es als Schlüssel für eineHashMap:

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }
}

Jetzt fügen wir doppeltePerson-Objekte inMap ein, die diesen Schlüssel verwenden.

Denken Sie daran, dass einMap keine doppelten Schlüssel enthalten darf:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

Hier verwenden wirPerson als Schlüssel. DaMap keine doppelten Schlüssel zulässt, sollten die zahlreichen doppeltenPerson-Objekte, die wir als Schlüssel eingefügt haben, den Speicher nicht vergrößern.

Abersince we haven’t defined proper equals() method, the duplicate objects pile up and increase the memory, deshalb sehen wir mehr als ein Objekt im Speicher. Der Heap-Speicher in VisualVM sieht dazu folgendermaßen aus:

 

image

if we had overridden the equals() and hashCode() methods properly, then there would only exist one Person object in this*Map*.

Werfen wir einen Blick auf die richtigen Implementierungen vonequals() undhashCode() für unsere KlassePerson:

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}

In diesem Fall stimmen die folgenden Aussagen:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map map = new HashMap<>();
    for(int i=0; i<2; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertTrue(map.size() == 1);
}

Nach dem korrekten Überschreiben vonequals() undhashCode() sieht der Heapspeicher für dasselbe Programm folgendermaßen aus:

 

image

Ein weiteres Beispiel ist die Verwendung eines ORM-Tools wie Hibernate, das die Methodenequals() undhashCode()verwendet, um die Objekte zu analysieren und im Cache zu speichern.

The chances of memory leak are quite high if these methods are not overridden, da der Ruhezustand dann keine Objekte vergleichen kann und seinen Cache mit doppelten Objekten füllen würde.

Wie kann man das verhindern?

  • Als Faustregel gilt, dass beim Definieren neuer Entitäten immer die Methodenequals() undhashCode() überschrieben werden

  • Es reicht nicht nur aus, um zu überschreiben, sondern diese Methoden müssen auch optimal überschrieben werden

Weitere Informationen finden Sie in unseren TutorialsGenerate equals() and hashCode() with Eclipse undGuide to hashCode() in Java.

3.4. Innere Klassen, die auf äußere Klassen verweisen

Dies ist bei nicht statischen inneren Klassen (anonymen Klassen) der Fall. Für die Initialisierung benötigen diese inneren Klassen immer eine Instanz der einschließenden Klasse.

Jede nicht statische innere Klasse hat standardmäßig einen impliziten Verweis auf ihre enthaltende Klasse. Wenn wir das Objekt dieser inneren Klasse in unserer Anwendung verwenden, danneven after our containing class' object goes out of scope, it will not be garbage collected.

Stellen Sie sich eine Klasse vor, die den Verweis auf viele sperrige Objekte enthält und eine nicht statische innere Klasse hat. Wenn wir nun ein Objekt nur für die innere Klasse erstellen, sieht das Speichermodell folgendermaßen aus:

 

image

Wenn wir jedoch nur die innere Klasse als statisch deklarieren, sieht dasselbe Speichermodell folgendermaßen aus:

image

Dies liegt daran, dass das innere Klassenobjekt implizit einen Verweis auf das äußere Klassenobjekt enthält, wodurch es zu einem ungültigen Kandidaten für die Garbage Collection wird. Das gleiche passiert bei anonymen Klassen.

Wie kann man das verhindern?

  • Wenn die innere Klasse keinen Zugriff auf die enthaltenen Klassenmitglieder benötigt, sollten Sie sie in einestatic-Klasse umwandeln

3.5. Durchfinalize() Methoden

Die Verwendung von Finalisierern ist eine weitere Quelle für mögliche Probleme mit Speicherverlusten. Immer wenn die Methodefinalize()einer Klasse überschrieben wird, werden sie vonobjects of that class aren’t instantly garbage collected. stattdessen vom GC zur Finalisierung in die Warteschlange gestellt, was zu einem späteren Zeitpunkt erfolgt.

Wenn der in der Methodefinalize()geschriebene Code nicht optimal ist und die Finalizer-Warteschlange nicht mit dem Java-Garbage-Collector mithalten kann, ist unsere Anwendung früher oder später dazu bestimmt,OutOfMemoryError zu erfüllen.

Um dies zu demonstrieren, nehmen wir an, dass wir eine Klasse haben, für die wir die Methodefinalize()überschrieben haben, und dass die Ausführung der Methode etwas Zeit in Anspruch nimmt. Wenn eine große Anzahl von Objekten dieser Klasse Datenmüll sammelt, sieht das in VisualVM folgendermaßen aus:

 

image

Wenn wir jedoch nur die überschriebenefinalize()-Methode entfernen, gibt dasselbe Programm die folgende Antwort:

image

Wie kann man das verhindern?

  • Wir sollten Finalisten immer meiden

Weitere Informationen zufinalize() finden Sie in Abschnitt 3 (Avoiding Finalizers) in ourGuide to the finalize Method in Java.

3.6. InternierteStrings

Der JavaString-Pool hatte in Java 7 eine wesentliche Änderung erfahren, als er von PermGen auf HeapSpace übertragen wurde. Für Anwendungen, die mit Version 6 und niedriger ausgeführt werden, sollten wir jedoch aufmerksamer sein, wenn wir mit großenStringsarbeiten.

If we read a huge massive String object, and call intern() on that object, then it goes to the string pool, which is located in PermGen (permanent memory) and will stay there as long as our application runs. Dies blockiert den Speicher und führt zu einem großen Speicherverlust in unserer Anwendung.

Das PermGen für diesen Fall in JVM 1.6 sieht in VisualVM folgendermaßen aus:

 

image

Im Gegensatz dazu sieht in einer Methode das PermGen folgendermaßen aus:

image

 

Wie kann man das verhindern?

  • Die einfachste Möglichkeit, dieses Problem zu beheben, besteht darin, ein Upgrade auf die neueste Java-Version durchzuführen, wenn der String-Pool ab Java-Version 7 auf HeapSpace verschoben wird

  • Wenn Sie an großenStrings arbeiten, vergrößern Sie den PermGen-Raum, um möglicheOutOfMemoryErrors zu vermeiden:

    -XX:MaxPermSize=512m

3.7. MitThreadLocals

ThreadLocal (ausführlich im Tutorial vonIntroduction to ThreadLocal in Javabeschrieben) ist ein Konstrukt, mit dem wir den Status eines bestimmten Threads isolieren und so die Thread-Sicherheit erreichen können.

Bei Verwendung dieses Konstrukts wirdeach thread will hold an implicit reference to its copy of a ThreadLocal variable and will maintain its own copy, instead of sharing the resource across multiple threads, as long as the thread is alive.

Trotz seiner Vorteile ist die Verwendung vonThreadLocal-Variablen umstritten, da sie berüchtigt sind, Speicherlecks einzuführen, wenn sie nicht ordnungsgemäß verwendet werden. Joshua Blochonce commented on thread local usage:

„Die verspätete Verwendung von Thread-Pools in Kombination mit der verspäteten Verwendung von Thread-Locals kann zu einer unbeabsichtigten Beibehaltung von Objekten führen, wie an vielen Stellen festgestellt wurde. Es ist jedoch nicht gerechtfertigt, den Thread-Einheimischen die Schuld zu geben. “

Speicherlecks mitThreadLocals

ThreadLocals sollen Müll sein, der gesammelt wird, sobald der Haltefaden nicht mehr lebt. Das Problem tritt jedoch auf, wennThreadLocals zusammen mit modernen Anwendungsservern verwendet werden.

Moderne Anwendungsserver verwenden einen Pool von Threads, um Anforderungen zu verarbeiten, anstatt neue zu erstellen (z. B.the Executor im Fall von Apache Tomcat). Darüber hinaus verwenden sie einen separaten Klassenlader.

DaThread Pools in Anwendungsservern nach dem Konzept der Thread-Wiederverwendung arbeiten, werden sie niemals durch Müll gesammelt, sondern für eine andere Anforderung wiederverwendet.

Wenn eine Klasse eineThreadLocal -Variable erstellt, diese jedoch nicht explizit entfernt, verbleibt eine Kopie dieses Objekts auch nach dem Stoppen der Webanwendung beim WorkerThread, wodurch verhindert wird, dass das Objekt vorhanden ist Müll gesammelt.

Wie kann man das verhindern?

  • Es wird empfohlen,ThreadLocals zu bereinigen, wenn sie nicht mehr verwendet werden.ThreadLocals stellen dieremove()-Methode bereit, mit der der aktuelle Thread-Wert für diese Variable entfernt wird

  • Do not use ThreadLocal.set(null) to clear the value - Der Wert wird nicht gelöscht, sondern es werden stattdessen dieMap nachgeschlagen, die dem aktuellen Thread zugeordnet sind, und das Schlüssel-Wert-Paar als aktueller Thread bzw.null festgelegt

  • Es ist sogar noch besser,ThreadLocal as als eine Ressource zu betrachten, die in einemfinally-Block geschlossen werden muss, um sicherzustellen, dass sie auch im Ausnahmefall immer geschlossen ist:

    try {
        threadLocal.set(System.nanoTime());
        //... further processing
    }
    finally {
        threadLocal.remove();
    }

4. Andere Strategien für den Umgang mit Speicherlecks

Obwohl es beim Umgang mit Speicherlecks keine einheitliche Lösung gibt, gibt es einige Möglichkeiten, wie wir diese Lecks minimieren können.

4.1. Profilerstellung aktivieren

Java-Profiler sind Tools, die die Speicherverluste in der Anwendung überwachen und diagnostizieren. Sie analysieren, was intern in unserer Anwendung vor sich geht - zum Beispiel, wie Speicher zugewiesen wird.

Mithilfe von Profilern können wir verschiedene Ansätze vergleichen und Bereiche finden, in denen wir unsere Ressourcen optimal nutzen können.

Wir haben in Abschnitt 3 dieses TutorialsJava VisualVM verwendet. In unserenGuide to Java Profilers erfahren Sie mehr über verschiedene Arten von Profilern wie Mission Control, JProfiler, YourKit, Java VisualVM und den Netbeans Profiler.

4.2. Ausführliche Müllabfuhr

Durch Aktivieren der ausführlichen Speicherbereinigung verfolgen wir die detaillierte Ablaufverfolgung des GC. Um dies zu ermöglichen, müssen wir unserer JVM-Konfiguration Folgendes hinzufügen:

-verbose:gc

Durch Hinzufügen dieses Parameters können wir die Details der Vorgänge in GC anzeigen:

image

 

4.3. Verwenden Sie Referenzobjekte, um Speicherverluste zu vermeiden

Wir können auch auf Referenzobjekte in Java zurückgreifen, die im Paketjava.lang.refenthalten sind, um Speicherverluste zu beheben. Wenn Sie das Paketjava.lang.refverwenden, anstatt direkt auf Objekte zu verweisen, verwenden wir spezielle Verweise auf Objekte, mit denen sie problemlos durch Müll gesammelt werden können.

Referenzwarteschlangen dienen dazu, uns auf Aktionen aufmerksam zu machen, die vom Garbage Collector ausgeführt werden. Weitere Informationen finden Sie im Beispiel-Tutorial vonSoft References in Java, insbesondere in Abschnitt 4.

4.4. Eclipse Memory Leak Warnungen

Bei Projekten mit JDK 1.5 und höher zeigt Eclipse Warnungen und Fehler an, wenn offensichtliche Fälle von Speicherverlusten auftreten. Wenn wir also in Eclipse entwickeln, können wir regelmäßig die Registerkarte "Probleme" aufrufen und Warnungen vor Speicherverlusten (falls vorhanden) beachten:

image

 

4.5. Benchmarking

Wir können die Leistung des Java-Codes messen und analysieren, indem wir Benchmarks ausführen. Auf diese Weise können wir die Leistung alternativer Ansätze vergleichen, um dieselbe Aufgabe zu erledigen. Dies kann uns helfen, einen besseren Ansatz zu wählen, und es kann uns helfen, Speicherplatz zu sparen.

Weitere Informationen zum Benchmarking finden Sie in unserem Tutorial zuMicrobenchmarking with Java.

4.6. Codeüberprüfungen

Schließlich haben wir immer die klassische Art, einen einfachen Code-Rundgang zu machen.

In einigen Fällen kann sogar diese trivial aussehende Methode dabei helfen, einige häufig auftretende Speicherverlustprobleme zu beseitigen.

5. Fazit

Laien können sich Speicherlecks als eine Krankheit vorstellen, die die Leistung unserer Anwendung beeinträchtigt, indem sie wichtige Speicherressourcen blockiert. Und wie bei allen anderen Krankheiten kann es zu tödlichen Abstürzen der Anwendung kommen, wenn sie nicht geheilt werden.

Speicherverluste sind schwierig zu lösen und erfordern eine komplizierte Beherrschung und Beherrschung der Java-Sprache. While dealing with memory leaks, there is no one-size-fits-all solution, as leaks can occur through a wide range of diverse events.

Wenn wir jedoch auf Best Practices zurückgreifen und regelmäßig strenge Code-Durchgänge und -Profile durchführen, können wir das Risiko von Speicherverlusten in unserer Anwendung minimieren.

Wie immer sind die in diesem Lernprogramm dargestellten Codefragmente zum Generieren der in diesem Lernprogramm dargestellten VisualVM-Antwortenon GitHub verfügbar.