Hardware- und Softwareentwicklung, Java

Was tun bei OutOfMemory – Teil 1: Grundlagen

OutOfMemory ist eines der am häufigsten auftretenden Probleme bei Java-basierten Webapplikationen. Die Ursachen dafür sind genau so vielschichtig wie die Möglichkeiten der Analyse. Ziel dieses Artikels ist es, sowohl die Grundlagen wie auch mögliche Lösungswege zu zeigen.

Bevor man jetzt leichtfertig die Software umprogrammiert (da man ja genau weiß, wo das Problem liegt), sollte man mit einer ausführlichen Analyse beginnen. Nicht selten kommt es vor, dass Software (Parameter der JVM) oder sogar das Betriebssystem nicht korrekt konfiguriert sind.

Die Process Size

Die Process Size ist der maximal verfügbare Speicher für den Prozess. Dieser ist abhängig von der Hardware und vom Betriebssystem. In einer 32 Bit-Architektur kann die Process Size maximal 4 GB sein. Für einen Java-Prozess gibt es folgende Bereiche, die im Speicher des Prozesses liegen (siehe Abbildung 1):

  • Java Heap – Der Heap ist der Speicher, in dem die Java Objekte erzeugt und verwaltet werden.
  • Permanent Generation – Die Permanent Generation ist der Speicher, in dem die JVM sich selber verwaltet. In diesem Speicher werden auch die Klassen durch den ClassLoader geladen.
  • Nativ Code – Jeder Java-Prozess benötigt auch Speicher für den Native Code. Das sind C-Bibliotheken des Betriebssystems für den Zugriff auf Systemressourcen. Zum Beispiel der Zugriff aufs Dateisystem.

Abbildung 1: Schematischer Aufbau der Speicherverteilung eines Java-Prozesses

Der Speicher für den Native Code ist nicht explizit einstellbar. Er ergibt sich aus der Differenz zwischen Process Size, Heap und Permanent Generation. Dieser Speicher darf nicht zu knapp bemessen sein, denn es kann auch OutOfMemory im Native Code geben.

Die Java Heap Size

Die Java Heap Size gibt die Größe des Speichers an, der für die Erzeugung und Verwaltung der Java-Objekte verwendet werden kann. Die Größe des Heaps ist begrenzt. Wird keine Größe explizit angegeben, wird die Defaulteinstellung verwendet. Diese variiert von Betriebssystem zu Betriebssystem. Wenn man wirklich sicher gehen möchte, stellt man die Größe explizit ein:

-Xms[Bytes]m

      : initiale Größe des Heaps beim Starten des Prozesses

-Xmx[Bytes]m

    : maximale Größe des Heaps über den gesamten Lebenszeitraum

Der Heap teilt sich in zwei große Bereiche: der New Generation und der Old Generation (siehe Abbildung 2).
Wie die Namen schon vermuten lassen, befinden sich in der New Generation die ganzen neuen, jungen und kurzlebigen Java-Objekte und in der Old Generation die langlebigen Java-Objekte.

Abbildung 2: Aufteilung der Speicherbereiche im Heap

 

Das Verhältnis zwischen New und Old Generation ist aus Speichersicht der größte Unterschied zwischen einer Desktop- und einer Serveranwendung. Eine Desktopanwendung läuft in der Regel einen Arbeitstag, also ca. 8 h. Eine Serveranwendung läuft 24 h, 7 Tage die Woche und das mehrere Monate. Daran kann man schon erkennen, dass es bei einer Desktopanwendung mehr jüngere Objekte und bei einer Serveranwendung mehr ältere Objekte gibt. Mit dem Servermodus stellt man das Verhältnis zwischen New und Old Generation auf 1:3 ein.

-server

    :Servermodus für die VM

Permanent Generation

Die Permanent Generation wird von der JVM für das Laden der Klassen durch den ClassLoader verwendet. Normalerweise ist die Defaulteinstellung der Größe ausreichend. Wenn allerdings viele Klassen dynamisch durch die Applikation geladen werden (z.B. durch Reflection), kann die Defaulteinstellung zu Problemen führen. Auch die Verwendung von Persistenz- oder Caching-Frameworks führt zu größerem Speicherverbrauch in der Permanent Generation.
Über folgende Parameter kann die Defaultgröße der Permanent Generation angepasst werden:

-XX:PermSize=[Bytes]m :

      initiale Größe der Permanent Generation

-XX:MaxPermSize=[Bytes]m :

    maximale Größe der Permanent Generation

Garbage Collection

Man unterscheidet bei der Garbage Collection in die („einfache“) Garbage Collection und in die Full Garbage Collection. Bei der Garbage Collection werden nicht mehr referenzierte Objekte aus der New Generation freigegeben. Des Weiteren werden noch „lebende“ Objekte, die durch mehrere Garbage Collection nicht freigegeben wurden, in die Old Generation kopiert. Bei einer Full Garbage Collection werden auch Objekte aus der Old Generation aufgeräumt.
Eine Full Garbage Collection ist sehr zeitintensiv (bei 2 GB Heap Size = mehrere Sekunden). Während dieser Zeit reagiert die Applikation nicht mehr.
Ist die Heap Size zu gering eingestellt, findet eine Full Garbage Collection nach der anderen statt. Dies führt zu einer sehr schlechten Performance und zu langen Antwortszeiten.
In einer Multiprozessor-Maschine ist es deshalb ratsam, die Garbage Collection („einfache“ und Full) parallel arbeiten zu lassen.

-XX:+UseParallelGC:

    parallele Garbage Collection und Full Garbage Collection

Ausblick

Nachdem in diesem Artikel die Grundlagen erläutert wurden, werden im nächsten Artikel konkrete Analyse-Möglichkeiten besprochen.

Links

Share on FacebookTweet about this on TwitterPin on Pinterest