Java Language
Java-Speicherverwaltung
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.
Wenn
Reference
sind, die auf das Objekt verweisen, werden diese Referenzen gelöscht, bevor das Objekt gelöscht wird.Wenn das Objekt finalizable ist, dann wird es fertig gestellt sein. Dies geschieht, bevor das Objekt gelöscht wird.
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.