Java Memory Management, met zijn ingebouwde garbage collection, is een van de beste prestaties van de taal. Het stelt ontwikkelaars in staat om nieuwe objecten te maken zonder zich zorgen te hoeven maken over de allocatie en deallocatie van geheugen, omdat de garbage collector automatisch geheugen terugwint voor hergebruik. Dit maakt snellere ontwikkeling mogelijk met minder boilerplate code, terwijl geheugenlekken en andere geheugen-gerelateerde problemen worden geëlimineerd. Althans in theorie.
Ironisch gezien lijkt de Java-afvalverzameling te goed te werken, door te veel objecten te maken en te verwijderen. De meeste problemen met geheugenbeheer worden opgelost, maar vaak ten koste van ernstige prestatieproblemen. Het aanpasbaar maken van garbage collection aan allerlei situaties heeft geleid tot een complex en moeilijk te optimaliseren systeem. Om vuilnisverzameling te begrijpen, moet je eerst begrijpen hoe geheugenbeheer in een Java Virtuele Machine (JVM) werkt.
Hoe Java vuilnisverzameling echt werkt
Veel mensen denken dat vuilnisverzameling dode objecten verzamelt en weggooit. In werkelijkheid doet Java vuilnisverzameling het tegenovergestelde! Levende objecten worden bijgehouden en al het andere wordt als vuilnis aangemerkt. Zoals je zult zien, kan dit fundamentele misverstand tot veel prestatieproblemen leiden.
Laten we beginnen met de heap, dat is het gebied van het geheugen dat wordt gebruikt voor dynamische toewijzing. In de meeste configuraties wijst het besturingssysteem de heap van tevoren toe aan de JVM terwijl het programma draait. Dit heeft een paar belangrijke gevolgen:
- Object creatie is sneller omdat globale synchronisatie met het besturingssysteem niet nodig is voor elk object. Een toewijzing claimt simpelweg een deel van een geheugen array en verplaatst de offset pointer naar voren (zie figuur 2.1). De volgende toewijzing begint bij deze offset en claimt het volgende deel van de array.
- Wanneer een object niet langer wordt gebruikt, vordert de vuilnisman het onderliggende geheugen op en hergebruikt het voor toekomstige object toewijzing. Dit betekent dat er geen expliciete verwijdering plaatsvindt en dat er geen geheugen aan het besturingssysteem wordt teruggegeven.
Figuur 2.1: Nieuwe objecten worden eenvoudigweg toegewezen aan het einde van de gebruikte heap.
Alle objecten worden toegewezen op het door de JVM beheerde heap-gebied. Elk object dat de ontwikkelaar gebruikt wordt op deze manier behandeld, inclusief klasse-objecten, statische variabelen, en zelfs de code zelf. Zolang er naar een object verwezen wordt, beschouwt de JVM het als levend. Zodra een object niet langer wordt gerefereerd en dus niet meer bereikbaar is voor de applicatiecode, verwijdert de garbage collector het en vordert het ongebruikte geheugen terug. Hoe eenvoudig dit ook klinkt, het roept een vraag op: wat is de eerste verwijzing in de boom?
Garbage-Collection Roots-The Source of All Object Trees
Elke objectboom moet een of meer root-objecten hebben. Zolang de applicatie die wortels kan bereiken, is de hele boom bereikbaar. Maar wanneer worden die root-objecten als bereikbaar beschouwd? Speciale objecten die garbage-collection roots (GC roots; zie figuur 2.2) worden genoemd, zijn altijd bereikbaar en dat geldt ook voor elk object dat een garbage-collection root aan zijn eigen root heeft.
Er zijn vier soorten GC roots in Java:
- Lokale variabelen worden in leven gehouden door de stack van een thread. Dit is geen echte virtuele referentie van een object en is dus niet zichtbaar. In alle opzichten zijn lokale variabelen GC-wortels.
- Actieve Java-threads worden altijd als levende objecten beschouwd en zijn dus GC-wortels. Dit is vooral belangrijk voor thread lokale variabelen.
- Statische variabelen worden gerefereerd door hun klassen. Dit feit maakt ze de facto GC roots. Klassen zelf kunnen garbage-collected worden, wat alle statische variabelen waarnaar verwezen wordt zou verwijderen. Dit is van speciaal belang wanneer we applicatieservers, OSGi containers of class loaders in het algemeen gebruiken. We zullen de gerelateerde problemen bespreken in de Problem Patterns sectie.
- JNI References zijn Java objecten die de native code heeft aangemaakt als onderdeel van een JNI call. Objecten die zo worden aangemaakt, worden speciaal behandeld omdat de JVM niet weet of de native code er al dan niet naar verwijst. Dergelijke objecten vertegenwoordigen een zeer speciale vorm van GC root, die we in meer detail zullen bekijken in de Problem Patterns sectie hieronder.
Figuur 2.2: GC-wortels zijn objecten die zelf door de JVM worden gerefereerd en er zo voor zorgen dat geen enkel ander object wordt opgehaald.
Een eenvoudige Java-toepassing heeft dus de volgende GC-wortels:
- Lokale variabelen in de hoofdmethode
- De hoofdthread
- Statische variabelen van de hoofdklasse
Marking and Sweeping Away Garbage
Om te bepalen welke objecten niet langer in gebruik zijn, voert de JVM met tussenpozen wat heel toepasselijk een mark-and-sweep-algoritme wordt genoemd, uit. Zoals je misschien intuïtief aanvoelt, is het een eenvoudig proces in twee stappen:
- Het algoritme doorloopt alle objectverwijzingen, beginnend bij de GC wortels, en markeert elk gevonden object als levend.
- Al het heap-geheugen dat niet in beslag wordt genomen door gemarkeerde objecten, wordt teruggehaald. Het wordt gewoon gemarkeerd als vrij, in wezen vrijgeveegd van ongebruikte objecten.
Garbage collection is bedoeld om de oorzaak van klassieke geheugenlekken te verwijderen: onbereikbare-maar-niet-verwijderde objecten in het geheugen. Dit werkt echter alleen voor geheugenlekken in de oorspronkelijke zin. Het is mogelijk om ongebruikte objecten te hebben die nog steeds bereikbaar zijn voor een applicatie omdat de ontwikkelaar simpelweg vergeten is om ze te verwijderen. Zulke objecten kunnen niet worden opgevangen. Erger nog, zo’n logisch geheugenlek kan door geen enkele software worden opgespoord (zie figuur 2.3). Zelfs de beste analyse software kan alleen maar verdachte objecten aanwijzen. We zullen de analyse van geheugenlekken onderzoeken in het gedeelte Analyse van de prestatie-impact van geheugengebruik en vuilnisophaling, hieronder.