Vor einigen Tagen kam hier im Büro die Frage auf, inwiefern eine Javaprogramm nach einem OutOfMemoryError noch funktioniert.
Das erste kleine Testprogramm war schnell geschrieben. Allerdings zeigte sich dabei, dass es gar nicht so leicht ist den Speicher “fast” voll zu machen um zu sehen was noch funktioniert. Alle StringBuffer, Listen, Vektoren usw. waren nicht geeignet. Diese Klassen arbeiten intern mit Arrays. Sobald ein Array voll ist, wird es durch ein neues mit der doppelten Größe ersetzt. Dabei wird kurzzeitig der doppelte Speicherplatz benötigt. Es ist also wahrscheinlich das der OutOfMemoryError bei genau diesem Vergrößerungsvorgang erzeugt wird. Danach wird das neue doppelt so große Array aber vom GarbageCollector(GC) weggeräumt. Dadurch wird Speicher freigegeben und es kann erstmal normal weitergearbeitet werden (es sei denn man versucht in die Liste nochmal etwas einzufügen, also eine erneute Verdoppelung).
Um dieses Problem zu umgehen, habe ich eine verkette Liste verwendet. Hier kann ich auf alle Elemente eine Referenz halten (damit der GC nicht aufräumt) und brauche kein Array und keine Art von Listenimplementation.
In dem Testprogramm selbst wird nun in zwei Ebenen den Fehler abgefangen. In der ersten Ebene wird noch eine Referenz auf das erste Element in der Liste gehalten. Der GC hat also keine Möglichkeit aufzuräumen bzw. Speicher freizugeben. Je nachdem wann der GC zuletzt gelaufen ist, können noch einige Zeilen in dem catch Block ausgeführt werden. Teilweise reicht schon die Ausgabe des StackTrace um einen erneuten OutOf MemoryError zu erzeugen.
Wird nach dem ersten OutOfMemoryError ein weitere Error erzeugt (in dem catch Block), so wird dieser von der zweiten Ebene abgefangen. Hier wird dann keine Referenz auf das erste Element mehr gehalten. So kann der GC den ganzen Speicher freiräumen und das Programm kann ein zweites mal ohne Einschränkungen gestartet werden. Die Anzahl der erzeugten Objekte in der Liste nimmt dabei nur vom ersten zum zweiten Durchlauf ab. Danach bleibt die Zahl konstant. Es wird also wirklich der komplette Speicher geleert.
import java.io.IOException;
public class Test {
public static void main(final String[] args) throws IOException {
while (true) {
try {
final ListItem firstElement = new ListItem();
ListItem lastElement = firstElement;
final Runtime r = Runtime.getRuntime();
int i = 0;
try {
while (true) {
while (true) {
System.out.println(countElemtns(firstElement));
System.out.println(i++ + " Used Memory: " + (r.totalMemory() - r.freeMemory()) / (1024 * 1024));
lastElement = addItem(lastElement, i);
}
}
} catch (final Throwable t) {
// An dieser Stelle reicht 1 Zeichen mehr im buffer um einen OutOfMemoryError zu erzeugen
// final String test = new String("Gefangen"); // das geht trotzdem
// System.out.println(test); // das auch
t.printStackTrace(); // manchmal reicht das schon zum kaputt machen
lastElement = addItem(lastElement, i++);// Unterschiedlich wie oft wir das brauchen um echt einen weiteren OutOfMemory zu erzeugen
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
lastElement = addItem(lastElement, i++);
System.out.println(countElemtns(firstElement));
System.out.println("Wird meist nicht erreicht");
System.out.print("Neustart mit Enter: ");
System.in.read();
}
} catch (final Throwable e) {
e.printStackTrace();
System.out.print("Neustart mit Enter: "); // Keine Referenz mehr auf 'buffer' daher neustart mit leerem speicher moeglich
System.in.read();
}
}
}
static ListItem addItem(ListItem item, final int i) {
final ListItem newItem = new ListItem();
newItem.s = "TEST" + i;
item.child = newItem;
item = newItem;
return item;
}
static int countElemtns(ListItem item) {
int i = 0;
while (true) {
if (item != null) {
i++;
item = item.child;
} else {
break;
}
}
return i;
}
static class ListItem {
ListItem child;
String s;
}
} |
Um die Laufzeit nicht unnötig lang zu machen sollte der Speicher beim starten des Programms möglichst klein sein:
java -Xmx2m -Xms2m Test
Es ist also tatsächlich möglich nach einem OutOfMemoryError weiterzuarbeiten. Hierfür muss nur sichergestellt werden, dass nach dem entstehen des Errors sofort Speicher freigegeben wir. Jede weitere Zeile Code kann sonst zum nächsten Fehler führen.
Interessant wäre noch zu wissen ob es möglich ist den Speicher soweit zu füllen, dass bereits der Aufruf des GC zu einem weiteren Error führt. Viel Speicher kann er nicht benötigen wenn bereits das printStackTrace zum nächsten Fehler führt. Ggf braucht er gar keinen zusätzlichen Speicher bei der Ausführung, sondern hat schon alle im Speicher nach dem ersten Start?
In dem Programm habe ich wohl gegen alle Regeln des sauberen Programmierens verstoßen. Als Programmieren sollte man nie in die Situation kommen mit Throwable bzw. Error Klassen zu arbeiten. Im Fall eines OutOfMemoryErrors kann man wie oben beschrieben tatsächlich weiterarbeiten wenn genug Speicher freigegeben wurde, aber z.B. ein Error durch einen Festplattendefekt wird wohl kaum durch den GC behoben (oder kann der GC Festplatten reparieren? ).
Nach der Entwicklung eines solchen Codes bitte das Händewaschen nicht vergessen.
Felix Breske