Mit ‘Java’ getaggte Artikel

OutOfMemoryError? …. da geht noch was!

Freitag, 28. August 2009

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


(Ver)rechnen mit Java – Double vs. BigDecimal

Freitag, 17. Juli 2009

Ein einfaches Beispiel:

System.out.println(3 * 0.7);

Die Ausgabe sollte, rein rechnerisch, 2.1 lauten. Probieren Sie es aus. Auf den meisten Rechnern wird das Ergebnis 2.0999999999999996 lauten. Java rechnet hier intern mit Näherungswerten. Das macht die Berechnung zwar performanter aber eben nicht exakt und zudem kann das Ergebnis auf verschiedenen Plattformen unterschiedlich ausfallen (siehe strictfp).

Rechnen mit Strings: BigDecimal

Abhilfe soll hier die Verwendung von java.math.BigDecimal schaffen. Doch auch hier sei Vorsicht geboten:

System.out.println(new BigDecimal(2.1));

Liefert die Ausgabe:
2.100000000000000088817841970012523233890533447265625

Das Beispiel zeigt deutlich mit welchen internen Werten gerechnet wird. Verwenden wir jedoch den Konstruktor mit String erhalten wir den genauen Wert und rechnen auch mit diesem:

System.out.println(new BigDecimal("2.1"));

Die Multiplikation

System.out.println(
   new BigDecimal("3").multiply(new BigDecimal("0.7")));

ergibt dann auch endlich wie erwartet 2.1 und das auf allen Plattformen.

Zum Vergleich zweier Werte sollte die Methode compareTo der equals Methode vorgezogen werden weil sonst scheinbar gleiche Werte mit unterschiedlicher Anzahl an Nachkommastellen (scale) nicht gleich sind.
Ein Beispiel:

new BigDecimal("2.0").equals(new BigDecimal("2.00"))
// false
new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0
// true

Hierzu noch eine kleine Besonderheit. Java unterscheidet 0.0 und -0.0

new Double(0.0).equals(new Double(-0.0))
// false

Bei Verwendung von BigDecimal und der compareTo Methode gibt es hier allerdings keine Unterscheidung.

Was aber tun, wenn die Werte nur als double vorliegen?
Dann kann man sich so behelfen:

BigDecimal value = new BigDecimal(Double.toString(doubleValue));

sollte jedoch die JavaDocs zur toString Methode der Klasse java.lang.Double berücksichtigen.

strictfp

Noch ein kurzer Hinweis zum Schlüsselwort strictfp (das wohl nur wenigen bekannt sein dürfte, da es selten genutzt wird). Mit strictfp können sowohl Klassen als auch Methoden gekennzeichnet werden und versprechen dadurch Rechengenauigkeit nach IEEE-Norm. D.h. zumindest schon mal, dass die Ergebnisse auf verschiedenen Plattform identisch sein werden. Das oben besprochenen Dilemma mit double und float besteht aber weiterhin, auf allen Plattformen wird somit gleich ungenau gerechnet.

Fazit

Zur genauen Berechnung sollte immer BigDecimal(String val) verwendet werden, auch wenn dies zu Lasten der Performance geht. Zudem bietet die Klasse BigDecimal eine Vielzahl praktischer Methoden.
Um sich eine grobe Vorstellung bezüglicher der Performance zu machen, habe ich die Rechnung 3.0 * 0.7 jeweils 100000000 mal mit BigDecimal und double durchgeführt. Dauer:
BigDecimal: 160016 ms
double: 109 ms
Also ein gravierender Unterschied.

Weiterführenden Themen wäre hier u.a. die Klasse java.math.MathContext wenn es um Rundung und die Genauigkeit der Nachkommastellen geht.

Viel Erfolg beim Rechnen.


Christof Aenderl


Diagramme mit JFreeChart

Freitag, 10. Juli 2009

Jeder wird früher oder später der Aufgabe gegenübergestellt Daten grafisch aufzubereiten: Ein Diagramm muss her um einen Erfolg oder eine Verteilung zu visualisieren. Dann sollte man einen Plan in der Tasche haben, das Diagramm effektiv zu erstellen; mein Plan heisst JFreeChart.

“Hello Chart”

Für die schnelle und einfache Erstellung von Diagrammen bietet sich die ChartFactory an. Die Klasse bietet einige häufig verwendete Diagramme zur Auswahl.
Die Factory-Methoden erwarten die rudimentären Informationen, die für ein Diagramm benötigt werden, wie z.B.:

  • Titel des Diagramms
  • Beschriftung der X- / Y-Achse
  • Diagrammdaten
  • Flag, ob eine Legende erstellt werden soll

Natürlich gibt es hier leichte Variationen, je nachdem welchen Diagrammtyp man erstellen möchte. Heraus kommt ein Chart-Objekt, das man dann weiter nutzen, verändern oder einfach nur in eine Grafik wandeln kann, um es in eine Datei zu schreiben.

DefaultCategoryDataset dataset = new DefaultCategoryDataset();
dataset.addValue(100.0, "Datenreihe", "eins");
dataset.addValue(50.0,  "Datenreihe", "zwei");
dataset.addValue(75.0,  "Datenreihe", "drei");
dataset.addValue(37.5,  "Datenreihe", "vier");
dataset.addValue(56.25, "Datenreihe", "fünf");
 
JFreeChart chart = ChartFactory.createBarChart(
		"Hello Chart", 
		"X-Achse", 
		"Y-Achse", 
		dataset, 
		PlotOrientation.VERTICAL, 
		true, 
		true, 
		true);
 
BufferedImage image = chart.createBufferedImage(300, 200);

Wie man in dem folgenden Diagramm erkennen kann, kann man mit dem vorstehenden, kurzen Code-Fragment ein ansprechendes Diagramm einfach erzeugen

linechart

Die Qual der Wahl

Wenn man sich die ChartFactory anschaut findet man dort über 30 create-Methoden; doch welche mag die Richtige für das eigene Ziel sein? Der einfachste Weg geht natürlich über das Diagramm das ich erstellen möchte:

  • Liniendiagramm
  • Balkendiagramm
  • Tortendiagramm
  • … und vieles mehr

Aber es gibt mehr als das Erscheinungsbild. Daher möchte ich hier 3 verschieden Arten von Datensätzen exemplarisch vorstellen, da die Datasets über die Verwendung und die Visualisierung im Detail entscheiden:

  • CategoryDataset: ein 2-dimensionaler Datensatz, mit beliebigen X-Werten (u.a. auch Texte). Dieser Datensatz kann gut zur Erstellung von Grafiken mit wenigen individuellen Spalten verwendet werden und findet bei vielen Diagrammen Verwendung.
  • XYDataset: ein 2-dimensionaler Datensatz mit numerischen X-Werten. Dieser Datensatz ist optimal wenn viele X-Werte genutzt werden. (Anmerkung des Authors: mein persönlicher Favorit)
  • PieDataset: ein 1-dimensionaler Datensatz zur Verwendung in Tortendiagrammen.

Kleider machen Leute

Die erstellten Diagramme müssen sich natürlich in das UI oder die CI integrieren. Daher ist es nicht nur eine Kunst eine Diagramm zu erstellen, sondern auch es gemäß den Vorgaben anzuzeigen: Schriften, Farben, Rahmenstärken, etc. Auch hier finden wir bei JFreeChart Unterstützung.

  1. 3D-Diagramm: Wenn es ein wenig extravaganter sein muss, bieten sich spezielle 3D-Diagrammtypen an. Einige sind sogar über die ChartFactory verfügbar.
  2. Themes: Man kann ein ChartTheme erzeugen, mit dem man alle Diagramme programmatisch im selben Design formatieren kann. Man sollte sich der Themes früh annehmen, um manuelle Formatierungen zu vermeiden.

Unendliche Weiten

JFreeChart kann aber mehr als nur einfache Diagramm erstellen. So sind auch Diagramme mit mehreren Datenreihen ebenso möglich wie interaktive Grafiken. Dem Interessierten sei hier die Demo (Webstart) ans Herz gelegt.

Ausnahmslos glücklich?

Ich bin sehr angetan von JFreeChart. Die Bibliothek ist sehr umfassend und lässt kaum einen Wunsch offen. Die JavaDocs sind sehr gut und helfen einem durch viele grosse und kleine Probleme. Jedoch ist es bei der Größe der Bibliothek manchmal sehr schwierig sich einen vollständigen Überblick zu verschaffen. Hier bietet das Team von JFreechart jedoch einen kostenpflichtigen Developer Guide an. Der abgeneigte Käufer wird zwar viele Beispiele zu Diagrammen und Lösungsstrategien im Internet finden, hier jedoch so manche Stunde in die Recherche stecken müssen.


Thomas Bader


Vorsicht im Umgang mit SortedSet

Mittwoch, 24. Juni 2009

Sortierte Mengen werden häufig als Implementierungen des Interfaces SortedSet umgesetzt. Dieses Interface sieht vor, dass die Elemente einer solchen Menge entweder gemäß ihrer natürlichen Ordnung (durch die compareTo-Methode von Comparable definiert) oder durch einen Comparator sortiert werden. Manch einer übersieht dabei aber eine wichtige Voraussetzung an die compareTo- bzw. compare-Methode: Sie muß mit der equals-Methode konsistent sein, d.h. für zwei Objekte a und b muß a.equals(b) genau dann true zurückliefern, wenn a.compareTo(b) (bzw. die compare-Methode des Comparators) den Wert 0 liefert. Wenn dies nicht der Fall ist, verhält sich die Collection möglicherweise nicht mehr wie eine Menge, d.h. es ist nicht mehr sichergestellt, dass

  1. es keine zwei (im Sinne von equals) gleichen Elemente in der Menge gibt.
  2. man ein Element, das sich (im Sinne von equals) von allen Elemten innerhalb der Collection unterscheidet, einfügen kann.

In diesem Fall wäre man mit einem gewöhnlichen Set, das man selbst mit Hilfe von Collections.sort() sortiert, besser beraten. Ein kleines Beispiel soll das Problem verdeutlichen:
Die Klasse Person überschreibt equals derart, dass Personen mit gleicher id als gleich betrachtet werden:

public class Person {
  private Integer id;
  private Integer age;
  public Integer getId() {
      return id;
  }
  public void setId(final Integer id) {
      this.id = id;
  }
  public Integer getAge() {
      return age;
  }
  public void setAge(final Integer age) {
    this.age = age;
  }
 
  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof Person && id==(((Person)obj).id))
      return true;
    else
      return false;
  }
 
  public Person(final Integer id, final Integer age) {
    this.id = id;
    this.age = age;
  }
}

Wir definieren nun einen Comparator, der Personen nach ihrem Alter vergleicht und legen mit dessen Hilfe ein SortedSet an, in das wir zwei verschiedene Personen einfügen:

import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
 
public class Example {
 
  public static void main(final String[] args) {
    Comparator comp = new Comparator() {
      @Override
      public int compare( Person o1, Person o2) {
        return o1.getAge().compareTo(o2.getAge());
      }
    };
    SortedSet exampleSet =
      new TreeSet(comp);
    exampleSet.add(new Person(1,25));
    exampleSet.add(new Person(2,25));
    System.out.println("Size: "+exampleSet.size());
  }
}

Die Ausgabe bestätigt: es wurde lediglich eine Person tatsächlich in die Menge aufgenommen, obwohl die Personen anhand der equals-Methode eindeutig unterschieden werden. Bei komplexeren Comparator-Klassen kann es durchaus sinnvoll sein, die Konsistenz von equals-Methode und Comparator mittels eines eigenen Tests sicherzustellen. Aber bereits ein sorgfältiger Blick zur rechten Zeit kann einen hier vor schwer auffindbaren Fehlern bewahren.


Malte Wulf