Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Generational Garbage Collection

Generational Garbage Collection
Memory Management 
Generational Garbage Collection
 
 

Java Magazin, February 2010
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

Mit diesem Beitrag starten wir eine Reihe von Artikeln über das Memory Management in Java.  Wir beginnen mit den Prinzipien der Generational Garbage Collection, die von Suns JVM seit dem JDK 1.3 verwendet wird. Dabei wird der Heap-Speicher in unterschiedlichen Heap-Bereiche aufgeteilt. Dahinter steht die Idee, dass jeder der Heap-Bereiche die Objekte einer definierten Alterklasse enthält (deshalb der Name: Generational Garbage Collection).  Die Bereiche werden mit jeweils anderen Allokations- und Garbage-Collection-Algorithmen verwaltet werden. Dabei sind die Algorithmen speziell auf das Alter der Objekte optimiert.

Wir werden uns in den nächsten Artikeln unsere Kolumne mit automatischem Memory Management in Java sowie seinen Auswirkungen beschäftigen.  Dabei wird es um Speicherallokation und Speicherfreigabe gehen, aber auch um „Memory Leaks“, also unerwünschte Speicherverschwendung, Out-of-Memory-Probleme, unzumutbar lange Garbage-Collection-Pausen, Diagnose-Möglichkeiten, Tuning-Strategien und Compiler-Optimierungen wie Stack Allocation und Escape Analysis.  Motiviert ist die Artikelserie auch durch eine neue technologische Entwicklung in der Sun-JVM.  Die Speicherverwaltung ist immer komplexer geworden und seit Java 6 Update 14 gibt es einen radikal neuen Garbage Collector („Garbage First“, auch kurz „G1“ genannt), der in Java 7 als Standard-Garbage-Collector vorgesehen ist.  Wie funktioniert der neue Garbage Collector?  Was bringt er?  Welche Probleme löst er?

Ehe wir jedoch auf den neuen G1-Collector eingehen, wollen wir erst einmal die altbekannten Garbage Collectoren erläutern.  Der G1-Collector ist nämlich äußerst komplex und deshalb schwer zu verstehen, wenn man die Historie nicht kennt, aus der er sich entwickelt hat.  Außerdem werden uns die alten Collectoren noch eine ganze Weile begleiten und stehen als Alternative zu G1 auch in Java 7 weiterhin zur Verfügung.

Nun mag man sich die Frage stellen, wozu man sich als Java-Entwickler überhaupt mit der Speicherverwaltung in der virtuellen Maschine befassen sollte.  Solange es keine Probleme mit dem Anfordern und Freigeben von Speicher gibt, ist es in der Tat auch nicht erforderlich.  Wie aber die meisten aus der Praxis wissen, funktioniert nicht immer alles reibungslos. Bisweilen gibt es dann doch Probleme mit Speicherengpässen oder störenden Garbage-Collector-Pausen oder Performance-Probleme im Zusammenhang mit der Speicherverwaltung.  Es hilft dann bei der Diagnose und Problemlösung, wenn Kenntnisse über die Funktionsweise der Speicherverwaltung und des Garbage Collectors der virtuellen Maschine vorhanden sind.  Deshalb wollen wir das Thema „Speicherverwaltung in Java“ in der Artikelreihe näher beleuchten.

Wir beginnen die Artikelreihe mit den Prinzipien der sogenannten Generational Garbage Collection.   Warum wird der Speicher in „Generationen“ aufgeteilt?  Was sind diese Generationen?  Welche Generationen gibt es in der Sun JVM?
 

Generational Garbage Collection

Alle uns bekannten, heutigen Garbage Collectoren in den virtuellen Maschinen für Java (von Sun) sind Generational Garbage Collectoren; dieser Ansatz hat sich in der Praxis als der beste Algorithmus für Garbage Collection in Java erwiesen. Die Idee der Generational Garbage Collection besteht darin, dass man den Heap-Speicher in verschiedene Bereiche einteilt, die jeweils Objekte unterschiedlicher Alterklassen enthalten.  Mit speziellen auf das Alter der Objekte abgestimmten Algorithmen werden die jeweiligen Bereiche verwaltet und aufgeräumt.

Da man in Java (anders als beispielsweise in C++) keine Objekte auf dem Stack anlegen kann, müssen alle Objekte auf dem Heap alloziert werden. Selbst Objekte, die nur lokal in einer Methode gebraucht werden, entstehen auf dem Heap.  Die meisten dieser Objekte leben nicht lange: beim Verlassen der Methode werden sie bereits unerreichbar und sind damit „tot“.  Es gibt aber auch Heap-Objekte, die leben bis zum Ende der virtuellen Maschine, weil sie für die gesamte Ablaufzeit der Applikation gebraucht werden, zum Beispiel final static Attribute einer Klasse.

Insgesamt beobachtet man, dass die Objekt-Population eines typischen Java-Programms in etwa so aussieht, wie in Abbildung 1 gezeigt.
 
 


Abbildung 1: Typische Objekt-Population in Java






Es gibt sehr viele Java-Objekte, die nicht sehr alt werden, und es gibt vergleichsweise wenig Objekte, die sehr lange leben.  Die Objekte mit kurzer Lebensdauer sind die, die nur innerhalb einer Methode oder manchmal nur innerhalb einer Expression gebraucht werden.  Beispiele sind Iteratoren in einer Schleife oder StringBuilder fürs Zusammensetzen eines Strings.  Die Objekte mit mittlerer Lebensdauer sind solche, die in nicht-stackbasierten Verarbeitungen benutzt werden.  Ein Beispiel wäre der Conversational Session State einer Entity Bean.  Er überlebt mehrere Methodenaufrufe und wird erst nach der Session nicht mehr gebraucht.  Man beachte, die Zahl der Objekte mit mittlerer Lebensdauer ist deutlich geringer als die der kurzlebigen Objekte.  Richtig alt werden nur ganz wenige Objekte.  Das sind typischerweise Objekte, die schon beim Programmstart (oder per Lazy Evaluation etwas später) erzeugt werden und dann bis zum Ende der Anwendung in Gebrauch bleiben.  Dazu gehören Thread Pools, Singletons und Objekte aus Frameworks, z.B. Servlet-Instanzen.

Um den unterschiedlichen Objekt-Lebenszeiten angemessen Rechnung zu tragen, werden die Objekte verschiedenen Generationen zugeteilt.  Die Generationen sind separate Bereiche des Heaps, die unterschiedlich verwaltet werden.  Sie haben unterschiedliche Allokatoren und sie werden unterschiedlich oft und mit jeweils eigenen Garbage-Collection-Algorithmen aufgeräumt werden.  Im Wesentlichen unterscheidet man zwischen einer Young Generation, die häufig bereinigt wird, und einer Old Generation, die seltener bereinigt wird.  Die Young Generation ist der Speicherbereich, in dem die neuen Objekte angelegt werden, z.B. wenn im Programm mit new Speicher angefordert wird.  Die Old Generation ist der Bereich, in den die jungen Objekte ausgelagert werden, wenn sie ein gewisses Alter erreicht haben, d.h. wenn sie eine bestimmte Anzahl von Garbage Collections überlebt haben.

Warum ist es sinnvoll, dass die beiden Generationen unterschiedlich häufig und auch mit unterschiedlichen Algorithmen aufgeräumt werden? Dahinter steht die Beobachtung, dass von den jungen Objekten die meisten relativ schnell sterben, wie man in der Abbildung 1 gesehen hat, sodass in der Young Generation schnell viel Garbage entsteht.  Da, wo viel Garbage entsteht, lohnt sich zügiges Aufräumen, weil dann rasch wieder freier Speicher zur Verfügung steht.  Die Aufräumarbeiten auf die Young Generation zu beschränken, hat außerdem den Vorteil, dass nicht immer der gesamte Heap nach „toten“ Objekten durchforstet werden muss, sondern dass bereits das Aufräumen in einem Teilbereicht des Heaps signifikante Mengen an Speicher freigibt.
In der Old Generation lohnt sich das häufige Aufräumen nicht.  Wenn ein Objekt erst einmal mittelalt geworden ist, dann wird es mit hoher Wahrscheinlichkeit auch noch älter.  In der Old Generation findet man deshalb nicht so häufig „tote“ Objekte und damit weniger Potential für die Speicherfreigabe. Also macht man die Bereinigung der Old Generation wesentlich seltener.
Die Bereinigungen in der Young Generation werden als Minor Collections bezeichnet.  Daneben gibt es die seltener durchgeführten Major oder Full Collections, bei denen sowohl die Young als auch die Old Generation bereinigt wird. Diese Unterscheidung kann man bereits sehen, wenn man die virtuelle Maschine mit der Option –verbose:GC aufruft.  Die virtuelle Maschine gibt dann Information über die Garbage Collection aus (siehe Abbildung 2).
 
 


Abbildung 2: Garbage Collection Information per JVM-Option -verbose:GC

Man kann sehen, dass eine Full Garbage Collection selten durchgeführt wird, deutlich länger dauert als die Minor Garbage Collections, aber keineswegs mehr Speicher freischaufelt als eine Minor Garbage Collection.

Die Heapaufteilung in einer Sun-JVM

In der Sun JVM ist seit Java 1.3 der Speicher in 2 Generationen (young und old) und einen Sonderbereich (perm) eingeteilt (siehe Abbildung 3).


Abbildung 3: Heap-Aufteilung in einer Sun JVM



Perm-Space

Der Perm-Bereich ist keine Generation, sondern ein Non-Heap-Bereich. Er hat mit der Generational Garbage Collection und der Alterung der Objekte nichts zu tun.  Vielmehr wird er von der JVM für eigene Zwecke verwendet, um dort beispielsweise die Class-Objekte für alle geladenen Klassen inklusive Bytecodes abzulegen, oder auch interne JVM-Objekte (z.B. java/lang/Object or java/lang/exception) oder Informationen, die vom JIT-Compiler für Optimierungen benutzt werden. Von diesen Perm-Bereichen kann es auch mehrere geben, „shared“-Bereiche für Daten, die von mehreren JVMs gemeinsam benutzt werden, Bereiche für JVM-spezifische Daten und Bereiche für Daten, die im Zusammenhang mit Native Code gebraucht werden. Details zum Perm-Bereich findet man z.B. in Jon Masamitsu's Weblog (siehe / PERM /).

Diese Non-Heap-Bereiche haben mit der Generational Garbage Collection nichts zu tun. Die Heap-Bereiche, in denen die Applikation ihre Objekte ablegt, sind die Young und die Old Generation.

Young Generation

Die Young Generation zerfällt in 3 Unterbereiche: einen Bereich, der als Eden bezeichnet wird, und zwei Survivor Spaces.  Eden ist der Teil des Heaps, aus dem der new-Operator (oder das newInstance() per Reflection) den Speicher für neu erzeugte Objekte alloziert.  Das heißt, alle neuen Objekte entstehen in Eden.  Das stimmt nicht hunderprozentig; wenn Objekte riesig sind, z.B. größer als Eden, dann werden sie direkt in der Old Generation angelegt.  Aber das ist ein Sonderfall, den wir hier mal vernachlässigen wollen.  Prinzipiell entstehen alle neuen Objekte in Eden.
Von den beiden Survivor-Bereichen ist grundsätzlich einer immer leer. Man bezeichnet den benutzten Bereich als from-Space und den leeren Bereich als to-Space. Die Namen stammen daher, dass während einer Garbage Collection Objekte vom from-Space in den leeren to-Space kopiert werden.

Wenn nämlich im Rahmen einer Minor Garbage Collection überlebende Objekte in Eden und dem from-Space gefunden werden, so werden all diese überlebenden Objekte in den to-Space kopiert.  Danach steht der vormals belegte Speicher (Eden und der from-Space) als Ganzes wieder zur Verfügung.  Dabei wechseln from- und to-Space ihre Rollen.  Jetzt entstehen wieder Objekte in Eden, bis bei der nächsten Garbage Collection der Zyklus von vorne beginnt.

Ehe wir uns im nächsten Beitrag den Garbage-Collection-Algorithmus auf der Young Generation näher ansehen, werfen wir noch einen kurzen Blick auf die Old Generation.

Old Generation

Die Old Generation wird auch Tenured Generation genannt.  Sie enthält Objekte, die gealtert sind, d.h. die mehrere Garbage Collections auf der Young Generation überlebt haben.  Es gibt einen Schwellenwert; er wird Tenuring  Threshold genannt.  Das ist die Altersgrenze, nach deren Erreichen ein Objekt zu alt für die Young Generation ist und in die Old Generation verlagert wird.  Dieses Verschieben von der Young in die Old Generation nennt man Promotion.
Objekte können auch aus anderen Gründen von der Young in die Old Generation wandern.  Das ist beispielsweise der Fall, wenn so viele überlebende Objekte in der Young Generation übrig bleiben, dass sie nicht den Survivor Space passen.  Dann werden sie alle in die Old Generation geschoben.
Die Old Generation ist in der Regel deutlich größer als die Young Generation.  Ihr Füllgrad ändert sich nicht sehr heftig, weil Objekte in der Old Generation nur durch Promotion von Objekten aus der Young Generation entstehen und das passiert wiederum nur in Zusammenhang mit einer Garbage Collection auf der Young Generation.
Der Füllgrad der Young Generation hingegen ändert sich rasant, weil die Applikation mit allen ihren parallelen Threads die ganze Zeit mit new neue Objekte in Eden erzeugt.  Insbesondere Applikation, die stark parallelisiert arbeiten, erzeugen u.U. mächtige Last auf den Garbage Collector der Young Generation, der sich beeilen muss, möglichst ebenso rasch die überlebenden Ojekte in den to-Space oder die Old Generation zu kopieren.  In der Old Generation geht es dagegen vergleichsweise gemächlich zu.  Da kommen nicht ständig neue Objekte hinzu, sie sterben nicht so schnell und es muss auch nicht so oft aufgeräumt werden.
 

Vor- und Nachteil der Generational Garbage Collection

Was ist nun der Zweck dieser Einteilung des Heaps in Generationen?  Der Vorteil, den man sich von der Einteilung verspricht, ist eine gezielte und effiziente Speicherverwaltung auf den jeweiligen Bereichen.  Die Alternative zur Generational Garbage Collection wäre es, nur einen einzigen großen Heap zu verwalten mit genau einem Allokations- und einem Garbage Collection Algorithmus.  Frühe Garbage Collectoren haben auch genau so ausgesehen.  Dann werden alle Objekte gleich behandelt. Wenn man aber weiß, dass einige Objekte früh sterben und andere erst spät, dann kann man sich darauf gezielt einstellen.

Wie oben schon beschrieben, ist die Young Generation der Bereich, in dem viel alloziert und rasch gestorben wird.  Auf so einem Bereich ist es wichtig, dass Allokation und Garbage Collection schnell sind.  Auf der Old Generation wird wenig alloziert und selten gestorben, sodass die Geschwindigkeit der Algorithmen dort nicht so wichtig ist.  Alle Speicherverwaltungs-Algorithmen verursachen Kosten und Aufwände zu Lasten der Applikation.  Die Einteilung in Generationen ermöglicht es, die Prioritäten für die unterschiedlichen Bereiche spezifisch zu setzen.
Zu den Kosten gehört zum Beispiel: wie effizient geht ein Algorithmus mit dem Speicher um?  Wie wir im nächsten Beitrag sehen werden, brauchen die schnellen Allokations- und Garbage-Collection-Algorithmen auf der Young Generation relativ mehr Speicher als die langsamen Algorithmen auf der Old Generation.
Eine anderer Aspekt ist: wie sehr stört der Algorithmus meine Applikation?  Manche Garbage-Collection-Algorithmen müssen sämtliche Applikations-Threads stoppen, damit sie arbeiten können; das sind sogenannte Stop-the-World-Pausen, die äußerst störend sein können.  Es gibt andere Algorithmen, die können zumindest teilweise konkurrierend zur Applikation laufen und verursachen nur wenige kurze Pausen.

Wieder ein anderer Aspekt ist: wieviel Overhead erzeugt ein Algorithmus?  Manche Garbage-Collection-Algorithmen brauchen Unterstützung, sobald Referenzen gelesen oder verändert werden; das sind sogenannte Read- bzw. Write-Barriers.  So eine Barrier ist ein Stückchen Code, das bei Zugriffen auf Referenzen ausgeführt wird und dann irgendwas macht, was später der Allokations- oder der Garbage-Collection-Algorithmus braucht.  Das erzeugt zwar keine störenden Pausen, aber verlangsamt natürlich die Applikation insgesamt.  Manche Algorithmen hingegen kommen ganz ohne Barrier aus.

Die Einteilung in Generationen hat den Vorteil, dass man genau den Algorithmus wählen kann, der nach allen Kosten-Nutzen-Erwägungen am besten zum Alter der Objekte in der jeweiligen Generation passt.  Das setzt natürlich voraus, dass die Objektpopulation auch tatsächlich die erwarteten Eigenschaften hat.  In einem pathologischen Falle, wo die Applikation eben keine hohe „Kindersterblichkeit“ hat, wird die Generational Garbage Collection auch nicht so effizient sein.  Natürlich ist eine Generational Garbage Collection komplizierter und damit auch „teuerer“ als eine einfache Garbage Collection auf nur einem einzigen großen Heap mit nur einem einzigen Algorithmus.  Normalerweise werden die Kosten für die Komplikationen durch die Vorteile kompensiert.   Wie gesagt, in pathologischen Fällen kann es aber auch mal anders sein.

Wie der passende Algorithmus auswählt wird, hängt übrigens von der jeweiligen JVM-Implementierung ab.  Meistens sind die Aufteilung des Heaps und die Auswahl der Algorithmen in der JVM-Implementierung fest verdrahtet.  In gewissem Rahmen hat der Java-Anwender Einfluss, soweit eine JVM entsprechende Steuerungsmöglichkeiten über JVM-Optionen zur Verfügung stellt.  In der Sun-JVM gibt es zahllose Einstellschrauben, die wir im Verlauf dieser Artikelreihe zum Teil erwähnen werden.  Der Java-Anwender kann z.B. die Größe der Generation oder auch gewisse Details der Algorithmen einstellen.  Es gibt auch selbstadaptive Strategien, bei denen der Java-Anwender nur noch ein Ziel (minimaler Durchsatz oder maximale Pause) vorgibt, und die Garbage Collection stellt sich selbst so ein, dass das Ziel möglichst erreicht wird.  Das wird in der Sun-JVM als Garbage Collection Ergonomics bezeichnet.  Aber eine generelle Auswahl zwischen „Generational Garbage Collection – ja oder nein“ hat man in der Sun-JVM nicht.

Erst seit Java 6 Update 14 kann er alternativ zur Generational Garbage Collection den neuen (in Java 6 noch experimentellen) G1-Collector verwenden.
Wir werden ihn später ausführlich besprechen.  In den nächsten Beiträgen wollen wir aber erst einmal die Algorithmen auf der Young und der Old Generation erläutern, damit sich ein Java-Anwender im Falle von Problemen mit der Speicherverwaltung in Java 5 oder 6 überlegen kann, woran es wohl liegen könnte und was er tun könnte, um das Problem zu lösen.

Zusammenfassung

In diesem Beitrag haben wir uns angesehen,  wie der Heap-Speicher in einer Sun JVM aufgeteilt ist: es gibt eine Young und eine Old Generation.  Diese Aufteilung ist die Basis für die Generational Garbage Collection, bei der versucht wird, der unterschiedlichen Lebensdauer der Objekte in einer Java-Applikation Rechnung zu tragen und auf unterschiedlichen Heap-Bereichen unterschiedlich Allokations- und Garbage-Collection-Algorithmen zu verwenden.  Welche Algorithmen das sind und wie sie funktionieren, besprechen wir in den nächsten Beiträgen.
 

Literaturverweise

/SUN/ Memory Management in the Java HotSpot™ Virtual Machine
Sun Microsystems, April 2006
URL: http://java.sun.com/j2se/reference/whitepapers/memorymanagement_whitepaper.pdf
/PERM/  Presenting the Permanent Generation
Jon Masamitsu's Weblog, November 2006
URL: http://blogs.sun.com/jonthecollector/entry/presenting_the_permanent_generation

Die gesamte Serie über Garbage Collection:

/GC1/ Generational Garbage Collection
Klaus Kreft & Angelika Langer, Java Magazin, February 2010
URL: http://www.angelikalanger.com/Articles/EffectiveJava/49.GC.GenerationalGC/49.GC.GenerationalGC.html
/GC2/ Young Generation Garbage Collection
Klaus Kreft & Angelika Langer, Java Magazin, April 2010
URL: http://www.angelikalanger.com/Articles/EffectiveJava/50.GCYoungGenGC/50.GC.YoungGenGC.html
/GC3/ Old Generation Garbage Collection – Mark-and-Compact
Klaus Kreft & Angelika Langer, Java Magazin, June 2010
URL: http://www.angelikalanger.com/Articles/EffectiveJava/51.GC.OldGen.MarkCompact/51.GC.OldGen.MarkCompact.html
/GC4/ Old Generation Garbage Collection – Concurrent-Mark-Sweep (CMS)
Klaus Kreft & Angelika Langer, Java Magazin, August 2010
URL: http://www.angelikalanger.com/Articles/EffectiveJava/52.GC.OldGen.CMS/52.GC.OldGen.CMS.html
/GC5/ Garbage Collection Tuning – Die Ziele
Klaus Kreft & Angelika Langer, Java Magazin, Oktober 2010
URL: http://www.angelikalanger.com/Articles/EffectiveJava/53.GC.Tuning.Goals/53.GC.Tuning.Goals.html
/GC6/ Garbage Collection Tuning – Die Details
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2010
URL: http://www.angelikalanger.com/Articles/EffectiveJava/54.GC.Tuning.Details/54.GC.Tuning.Details.html
/GC7/ Der Garbage-First Garbage Collector (G1) - Übersicht über die Funktionalität
Klaus Kreft & Angelika Langer, Java Magazin, Februar 2011
URL: http://www.angelikalanger.com/Articles/EffectiveJava/55.GC.G1.Overview/55.GC.G1.Overview.html
/GC8/ Der Garbage-First Garbage Collector (G1) - Details und Tuning
Klaus Kreft & Angelika Langer, Java Magazin, April 2011
URL: http://www.angelikalanger.com/Articles/EffectiveJava/56.GC.G1.Details/56.GC.G1.Details.html
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
High-Performance Java - programming, monitoring, profiling, and tuning techniques
4 day seminar ( open enrollment and on-site)
Garbage Collection Tuning - profiling and tuning of memory related performance issues
1 day seminar ( open enrollment and on-site)
 
  © Copyright 1995-2012 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/49.GC.GenerationalGC/49.GC.GenerationalGC.html  last update: 1 Mar 2012