|
|||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||
|
Generational Garbage Collection
|
||||||||||||||||||||||||||||
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 CollectionAlle 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.
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.
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-JVMIn der Sun JVM ist seit Java 1.3 der Speicher in 2 Generationen (young und old) und einen Sonderbereich (perm) eingeteilt (siehe Abbildung 3).
Perm-SpaceDer 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 GenerationDie 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 GenerationDie 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 CollectionWas 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.
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.
ZusammenfassungIn 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
Die gesamte Serie über Garbage Collection:
|
|||||||||||||||||||||||||||||
© 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 |