Inzicht in geheugenlekken in Java

1. Inleiding

Een van de belangrijkste voordelen van Java is het geautomatiseerde geheugenbeheer met behulp van de ingebouwde Garbage Collector (of GC in het kort). De GC zorgt impliciet voor het toewijzen en vrijmaken van geheugen en is dus in staat om de meeste problemen met geheugenlekken op te lossen.

Hoewel de GC effectief een groot deel van het geheugen verwerkt, is het geen garantie voor een waterdichte oplossing voor geheugenlekken. De GC is behoorlijk slim, maar niet onberispelijk. Geheugenlekken kunnen nog steeds sluipen, zelfs in toepassingen van een gewetensvolle ontwikkelaar.

Er kunnen nog steeds situaties zijn waarin de toepassing een aanzienlijk aantal overbodige objecten genereert, waardoor cruciale geheugenbronnen worden uitgeput, wat soms kan resulteren in het falen van de hele toepassing.

Geheugenlekken zijn een echt probleem in Java. In deze tutorial zullen we zien wat de mogelijke oorzaken van geheugenlekken zijn, hoe ze tijdens runtime kunnen worden herkend en hoe ze in onze applicatie kunnen worden aangepakt.

2. Wat is een geheugenlek

Een geheugenlek is een situatie wanneer er objecten in de hoop aanwezig zijn die niet langer worden gebruikt, maar de garbage collector ze niet uit het geheugen kan verwijderen en dus worden ze onnodig onderhouden.

Een geheugenlek is slecht omdat het blokkeert geheugenbronnen en verslechtert de systeemprestaties in de loop van de tijd. En als het niet wordt behandeld, zal de toepassing uiteindelijk zijn bronnen uitputten en uiteindelijk eindigen met een fatale afloop java.lang.OutOfMemoryError.

Er zijn twee verschillende soorten objecten die zich in het Heap-geheugen bevinden: waarnaar wordt verwezen en niet. Objecten waarnaar wordt verwezen, zijn objecten die nog steeds actieve referenties hebben binnen de toepassing, terwijl objecten zonder referentie geen actieve referenties hebben.

De garbage collector verwijdert periodiek objecten zonder verwijzing, maar verzamelt nooit de objecten waarnaar nog steeds wordt verwezen. Dit is waar geheugenlekken kunnen optreden:

Symptomen van een geheugenlek

  • Ernstige prestatievermindering wanneer de applicatie gedurende lange tijd continu draait
  • Onvoldoende geheugen fout heap-fout in de applicatie
  • Spontane en vreemde applicatie crasht
  • De applicatie heeft af en toe geen verbindingsobjecten meer

Laten we enkele van deze scenario's eens nader bekijken en hoe we ermee om kunnen gaan.

3. Soorten geheugenlekken in Java

Bij elke toepassing kunnen geheugenlekken om verschillende redenen optreden. In dit gedeelte bespreken we de meest voorkomende.

3.1. Geheugen lekt door statisch Velden

Het eerste scenario dat een mogelijk geheugenlek kan veroorzaken, is intensief gebruik van statisch variabelen.

In Java, statisch velden hebben een levensduur die gewoonlijk overeenkomt met de volledige levensduur van de actieve toepassing (tenzij ClassLoader komt in aanmerking voor garbage collection).

Laten we een eenvoudig Java-programma maken dat een statischLijst:

openbare klasse StaticTest {openbare statische lijstlijst = nieuwe ArrayList (); public void populateList () {for (int i = 0; i <10000000; i ++) {list.add (Math.random ()); } Log.info ("Debug Point 2"); } public static void main (String [] args) {Log.info ("Debug Point 1"); nieuwe StaticTest (). populateList (); Log.info ("Debug Point 3"); }}

Als we nu het Heap-geheugen analyseren tijdens de uitvoering van dit programma, zullen we zien dat tussen debug-punten 1 en 2, zoals verwacht, het heap-geheugen is toegenomen.

Maar als we de populateList () methode op het debug-punt 3, het hoopgeheugen is nog geen garbagecollection zoals we kunnen zien in deze VisualVM-reactie:

Echter, in het bovenstaande programma, in regel nummer 2, als we het trefwoord gewoon laten vallen statisch, dan zal het een drastische verandering brengen in het geheugengebruik, deze Visual VM-reactie laat zien:

Het eerste deel tot het foutopsporingspunt is bijna hetzelfde als wat we in het geval van statisch. Maar deze keer nadat we de populateList () methode, al het geheugen van de lijst is verzamelde vuilnis omdat we er geen enkele verwijzing naar hebben.

Daarom moeten we heel goed letten op ons gebruik van statisch variabelen. Als verzamelingen of grote objecten worden gedeclareerd als statisch, dan blijven ze in het geheugen gedurende de levensduur van de applicatie, waardoor het vitale geheugen wordt geblokkeerd dat anders elders zou kunnen worden gebruikt.

Hoe kan ik dit voorkomen?

  • Minimaliseer het gebruik van statisch variabelen
  • Vertrouw bij het gebruik van singletons op een implementatie die het object lui laadt in plaats van gretig te laden

3.2. Via niet-gesloten bronnen

Elke keer dat we een nieuwe verbinding maken of een stream openen, wijst de JVM geheugen toe voor deze bronnen. Enkele voorbeelden zijn databaseverbindingen, invoerstromen en sessieobjecten.

Als u deze bronnen vergeet te sluiten, kan het geheugen worden geblokkeerd, waardoor ze buiten het bereik van GC blijven. Dit kan zelfs gebeuren in het geval van een uitzondering die verhindert dat de programma-uitvoering de instructie bereikt die de code afhandelt om deze bronnen te sluiten.

In elk geval, de open verbinding overgebleven van bronnen verbruikt geheugen, en als we ze niet aanpakken, kunnen ze de prestaties verslechteren en zelfs resulteren in Onvoldoende geheugen fout.

Hoe kan ik dit voorkomen?

  • Gebruik altijd Tenslotte blok om bronnen te sluiten
  • De code (zelfs in de Tenslotte block) waarmee de bronnen worden gesloten, zouden zelf geen uitzonderingen moeten hebben
  • Bij gebruik van Java 7+ kunnen we gebruik maken van proberen-met-bronnenblok

3.3. Ongepast is gelijk aan () en hashCode () Implementaties

Bij het definiëren van nieuwe klassen is een veel voorkomende vergissing niet het schrijven van de juiste overschreven methoden voor is gelijk aan () en hashCode () methoden.

HashSet en Hash kaart gebruik deze methoden in veel bewerkingen, en als ze niet correct worden overschreven, kunnen ze een bron worden voor mogelijke geheugenlekproblemen.

Laten we een voorbeeld nemen van een triviaal Persoon class en gebruik deze als sleutel in een Hash kaart:

openbare klasse Persoon {naam openbare tekenreeks; openbare persoon (tekenreeksnaam) {this.name = naam; }}

Nu zullen we een duplicaat invoegen Persoon objecten in een Kaart die deze sleutel gebruikt.

Onthoud dat a Kaart mag geen dubbele sleutels bevatten:

@Test openbare leegte gegevenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); voor (int i = 0; i <100; i ++) {map.put (nieuwe persoon ("jon"), 1); } Assert.assertFalse (map.size () == 1); }

Hier gebruiken we Persoon als sleutel. Sinds Kaart staat geen dubbele sleutels toe, de talrijke duplicaten Persoon objecten die we als sleutel hebben ingevoegd, mogen het geheugen niet vergroten.

Maar aangezien we het niet juist hebben gedefinieerd is gelijk aan () methode stapelen de dubbele objecten zich op en vergroten het geheugen, daarom zien we meer dan één object in het geheugen. Het Heap-geheugen in VisualVM ziet er als volgt uit:

Echter, als we de is gelijk aan () en hashCode () methoden goed, dan zou er maar één bestaan Persoon object hierin Kaart.

Laten we eens kijken naar de juiste implementaties van is gelijk aan () en hashCode () voor onze Persoon klasse:

openbare klasse Persoon {naam openbare tekenreeks; openbare persoon (tekenreeksnaam) {this.name = naam; } @Override public boolean equals (Object o) {if (o == this) return true; if (! (o instanceof Person)) {return false; } Persoon persoon = (Persoon) o; return person.name.equals (naam); } @Override public int hashCode () {int resultaat = 17; resultaat = 31 * resultaat + naam.hashCode (); resultaat teruggeven; }}

En in dit geval zouden de volgende beweringen waar zijn:

@Test openbare leegte gegevenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); voor (int i = 0; i <2; i ++) {map.put (nieuwe persoon ("jon"), 1); } Assert.assertTrue (map.size () == 1); }

Na behoorlijk te hebben overwonnen is gelijk aan () en hashCode ()ziet het Heap-geheugen voor hetzelfde programma er als volgt uit:

Een ander voorbeeld is het gebruik van een ORM-tool zoals Hibernate, die gebruikmaakt van is gelijk aan () en hashCode () methoden om de objecten te analyseren en ze op te slaan in de cache.

De kans op een geheugenlek is vrij groot als deze methoden niet worden overschreven omdat Hibernate dan geen objecten zou kunnen vergelijken en zijn cache zou vullen met dubbele objecten.

Hoe kan ik dit voorkomen?

  • Als vuistregel geldt dat u altijd voorrang moet hebben bij het definiëren van nieuwe entiteiten is gelijk aan () en hashCode () methoden
  • Het is niet alleen voldoende om te overschrijven, maar deze methoden moeten ook op een optimale manier worden overschreven

Bezoek onze tutorials Generate voor meer informatie is gelijk aan () en hashCode () met Eclipse en Guide to hashCode () in Java.

3.4. Innerlijke klassen die verwijzen naar buitenste klassen

Dit gebeurt in het geval van niet-statische innerlijke klassen (anonieme klassen). Voor initialisatie hebben deze innerlijke klassen altijd een instantie van de omsluitende klasse nodig.

Elke niet-statische innerlijke klasse heeft standaard een impliciete verwijzing naar de bevattende klasse. Als we het object van deze innerlijke klasse in onze applicatie gebruiken, dan zelfs nadat het object van de bevattende klasse 'buiten het bereik valt, wordt het niet als garbage opgehaald.

Beschouw een klasse die de verwijzing naar veel omvangrijke objecten bevat en een niet-statische innerlijke klasse heeft. Wanneer we nu een object maken van alleen de innerlijke klasse, ziet het geheugenmodel er als volgt uit:

Als we de innerlijke klasse echter gewoon als statisch declareren, ziet hetzelfde geheugenmodel er als volgt uit:

Dit gebeurt omdat het binnenste klasseobject impliciet een verwijzing naar het buitenste klasseobject bevat, waardoor het een ongeldige kandidaat voor garbagecollection wordt. Hetzelfde gebeurt in het geval van anonieme lessen.

Hoe kan ik dit voorkomen?

  • Als de innerlijke klas geen toegang nodig heeft tot de groepsleden die erin zitten, overweeg dan om er een statisch klasse

3.5. Door afronden() Methoden

Het gebruik van finalizers is nog een andere bron van mogelijke problemen met geheugenlekken. Wanneer een klas ' afronden() methode wordt dan overschreven objecten van die klasse worden niet meteen garbagecollection verzameld. In plaats daarvan zet de GC ze in de wachtrij voor afronding, wat op een later tijdstip gebeurt.

Bovendien, als de code geschreven in afronden() methode is niet optimaal en als de finalizer-wachtrij de Java-garbagecollector niet kan bijhouden, dan is onze applicatie vroeg of laat bestemd om te voldoen aan een Onvoldoende geheugen fout.

Laten we, om dit aan te tonen, overwegen dat we een klasse hebben waarvoor we de afronden() methode en dat het wat tijd kost om de methode uit te voeren. Wanneer een groot aantal objecten van deze klasse garbage wordt opgehaald, ziet het er in VisualVM als volgt uit:

Als we echter het overschreven afronden() methode, dan geeft hetzelfde programma het volgende antwoord:

Hoe kan ik dit voorkomen?

  • We moeten finalizers altijd vermijden

Voor meer informatie over afronden(), lees paragraaf 3 (Finalizers vermijden) in onze Gids voor de afrondingsmethode in Java.

3.6. Geïnterneerd Snaren

De Java Draad pool had een grote verandering ondergaan in Java 7 toen het werd overgebracht van PermGen naar HeapSpace. Maar voor applicaties die op versie 6 en lager werken, moeten we meer opletten wanneer we met grote werken Snaren.

Als we een enorm massaal lezen Draad bezwaar maken en bellen intern() op dat object, dan gaat het naar de stringpool, die zich in PermGen (permanent geheugen) bevindt en daar blijft zolang onze applicatie draait. Dit blokkeert het geheugen en zorgt voor een groot geheugenlek in onze applicatie.

De PermGen voor dit geval in JVM 1.6 ziet er als volgt uit in VisualVM:

In tegenstelling hiermee, in een methode, als we gewoon een string uit een bestand lezen en deze niet interneren, ziet de PermGen er als volgt uit:

Hoe kan ik dit voorkomen?

  • De eenvoudigste manier om dit probleem op te lossen, is door te upgraden naar de nieuwste Java-versie, aangezien String-pool vanaf Java-versie 7 naar HeapSpace wordt verplaatst
  • Als u op grote Snaren, vergroot de grootte van de PermGen-ruimte om elk potentieel te vermijden OutOfMemoryErrors:
    -XX: MaxPermSize = 512m

3.7. Gebruik makend van ThreadLocals

ThreadLocal (gedetailleerd besproken in Inleiding tot ThreadLocal in Java-zelfstudie) is een constructie die ons de mogelijkheid geeft om de status van een bepaalde thread te isoleren en zo de veiligheid van de thread te bereiken.

Bij gebruik van dit construct, elke thread bevat een impliciete verwijzing naar zijn kopie van een ThreadLocal variabele en zal zijn eigen kopie behouden, in plaats van de bron over meerdere threads te delen, zolang de thread actief is.

Ondanks de voordelen is het gebruik van ThreadLocal variabelen zijn controversieel, omdat ze berucht zijn omdat ze geheugenlekken introduceren als ze niet correct worden gebruikt. Joshua Bloch heeft ooit gereageerd op lokaal gebruik van threads:

“Slordig gebruik van threadpools in combinatie met slordig gebruik van thread-locals kan onbedoeld objectretentie veroorzaken, zoals op veel plaatsen is opgemerkt. Maar het is niet gerechtvaardigd om de schuld te geven aan de lokale bevolking. "

Geheugen lekt met ThreadLocals

ThreadLocals worden verondersteld vuilnis te zijn die wordt verzameld zodra de vasthouddraad niet meer leeft. Maar het probleem doet zich voor wanneer ThreadLocals worden samen met moderne applicatieservers gebruikt.

Moderne applicatieservers gebruiken een pool van threads om verzoeken te verwerken in plaats van nieuwe te maken (bijvoorbeeld de Uitvoerder in het geval van Apache Tomcat). Bovendien gebruiken ze ook een aparte classloader.

Aangezien Thread Pools in applicatieservers werken volgens het concept van hergebruik van threads, worden ze nooit als afval verzameld, maar worden ze hergebruikt om aan een ander verzoek te voldoen.

Als een klasse nu een ThreadLocal variabele maar verwijdert het niet expliciet, dan blijft een kopie van dat object bij de werknemer Draad zelfs nadat de webapplicatie is gestopt, waardoor wordt voorkomen dat het object wordt opgehaald.

Hoe kan ik dit voorkomen?

  • Het is een goede gewoonte om op te ruimen ThreadLocals wanneer ze niet meer worden gebruikt - ThreadLocals lever de verwijderen() methode, die de huidige waarde van de thread voor deze variabele verwijdert
  • Gebruik niet ThreadLocal.set (null) om de waarde te wissen - het wist de waarde niet echt, maar zoekt in plaats daarvan de Kaart gekoppeld aan de huidige thread en stel het sleutel / waarde-paar in als de huidige thread en nul respectievelijk
  • Het is zelfs beter om te overwegen ThreadLocal als een resource die moet worden gesloten in een Tenslotte blok om er zeker van te zijn dat het altijd gesloten is, zelfs in het geval van een uitzondering:
    probeer {threadLocal.set (System.nanoTime ()); // ... verdere verwerking} eindelijk {threadLocal.remove (); }

4. Andere strategieën voor het omgaan met geheugenlekken

Hoewel er geen eenduidige oplossing is voor het omgaan met geheugenlekken, zijn er enkele manieren waarop we deze lekken kunnen minimaliseren.

4.1. Profilering inschakelen

Java-profilers zijn tools die de geheugenlekken via de applicatie monitoren en diagnosticeren. Ze analyseren wat er intern in onze applicatie gebeurt, bijvoorbeeld hoe geheugen wordt toegewezen.

Met behulp van profilers kunnen we verschillende benaderingen met elkaar vergelijken en gebieden vinden waar we onze middelen optimaal kunnen inzetten.

We hebben Java VisualVM gebruikt in sectie 3 van deze tutorial. Bekijk onze Guide to Java Profilers voor meer informatie over verschillende soorten profilers, zoals Mission Control, JProfiler, YourKit, Java VisualVM en de Netbeans Profiler.

4.2. Uitgebreide garbagecollection

Door uitgebreide garbage collection in te schakelen, volgen we een gedetailleerd trace van de GC. Om dit mogelijk te maken, moeten we het volgende aan onze JVM-configuratie toevoegen:

-verbose: gc

Door deze parameter toe te voegen, kunnen we de details zien van wat er in GC gebeurt:

4.3. Gebruik referentieobjecten om geheugenlekken te voorkomen

We kunnen ook gebruik maken van referentieobjecten in Java die zijn ingebouwd met java.lang.ref pakket om geheugenlekken op te lossen. Gebruik makend van java.lang.ref pakket, in plaats van rechtstreeks naar objecten te verwijzen, gebruiken we speciale verwijzingen naar objecten waarmee ze gemakkelijk kunnen worden opgehaald.

Referentiewachtrijen zijn bedoeld om ons bewust te maken van acties die worden uitgevoerd door de Garbage Collector. Voor meer informatie, lees Soft References in Java Baeldung tutorial, specifiek sectie 4.

4.4. Waarschuwingen voor Eclipse-geheugenlekken

Voor projecten op JDK 1.5 en hoger toont Eclipse waarschuwingen en fouten wanneer het duidelijke gevallen van geheugenlek tegenkomt. Dus bij het ontwikkelen in Eclipse kunnen we regelmatig het tabblad "Problemen" bezoeken en meer alert zijn op waarschuwingen voor geheugenlekken (indien van toepassing):

4.5. Benchmarking

We kunnen de prestaties van de Java-code meten en analyseren door benchmarks uit te voeren. Op deze manier kunnen we de prestaties van alternatieve benaderingen vergelijken om dezelfde taak uit te voeren. Dit kan ons helpen een betere benadering te kiezen en kan ons helpen geheugen te behouden.

Ga voor meer informatie over benchmarking naar onze Microbenchmarking met Java-zelfstudie.

4.6. Code beoordelingen

Ten slotte hebben we altijd de klassieke, ouderwetse manier om een ​​eenvoudige code-walk-through te doen.

In sommige gevallen kan zelfs deze triviaal ogende methode helpen bij het elimineren van enkele veelvoorkomende problemen met geheugenlekken.

5. Conclusie

In termen van de leek kunnen we geheugenlek beschouwen als een ziekte die de prestaties van onze applicatie verslechtert door vitale geheugenbronnen te blokkeren. En net als alle andere ziekten, kan het na verloop van tijd leiden tot fatale applicatiecrashes als het niet genezen wordt.

Geheugenlekken zijn lastig op te lossen en het vinden ervan vereist een ingewikkelde beheersing van en beheersing van de Java-taal. Bij het aanpakken van geheugenlekken is er geen eenduidige oplossing, aangezien lekken kunnen optreden door een breed scala aan uiteenlopende gebeurtenissen.

Als we echter onze toevlucht nemen tot best practices en regelmatig rigoureuze code-walk-throughs en profilering uitvoeren, kunnen we het risico van geheugenlekken in onze applicatie minimaliseren.

Zoals altijd zijn de codefragmenten die worden gebruikt om de VisualVM-reacties te genereren die in deze zelfstudie worden weergegeven, beschikbaar op GitHub.